MIT-Kurs "Computer Systems Security". Vorlesung 7: Die Native Client Sandbox, Teil 3

Massachusetts Institute of Technology. Vorlesung # 6.858. "Sicherheit von Computersystemen." Nikolai Zeldovich, James Mickens. 2014 Jahr


Computer Systems Security ist ein Kurs zur Entwicklung und Implementierung sicherer Computersysteme. Die Vorträge behandeln Bedrohungsmodelle, Angriffe, die die Sicherheit gefährden, und Sicherheitstechniken, die auf jüngsten wissenschaftlichen Arbeiten basieren. Zu den Themen gehören Betriebssystemsicherheit, Funktionen, Informationsflussmanagement, Sprachsicherheit, Netzwerkprotokolle, Hardwaresicherheit und Sicherheit von Webanwendungen.

Vorlesung 1: „Einführung: Bedrohungsmodelle“ Teil 1 / Teil 2 / Teil 3
Vorlesung 2: „Kontrolle von Hackerangriffen“ Teil 1 / Teil 2 / Teil 3
Vorlesung 3: „Pufferüberläufe: Exploits und Schutz“ Teil 1 / Teil 2 / Teil 3
Vorlesung 4: „Trennung von Privilegien“ Teil 1 / Teil 2 / Teil 3
Vorlesung 5: „Woher kommen Sicherheitssysteme?“ Teil 1 / Teil 2
Vorlesung 6: „Chancen“ Teil 1 / Teil 2 / Teil 3
Vorlesung 7: „Native Client Sandbox“ Teil 1 / Teil 2 / Teil 3

In Regel C4 gibt es eine Einschränkung. Sie können nicht über das Ende eines Programms "springen". Das Letzte, zu dem Sie springen können, ist die letzte Anweisung. Diese Regel garantiert also, dass bei der Ausführung des Programms in der Prozess-Engine keine Diskrepanz auftritt.

Regel C5 besagt, dass es keine Anweisungen geben darf, die größer als 32 Byte sind. Wir haben eine bestimmte Version dieser Regel in Betracht gezogen, als wir über die Vielzahl der Befehlsgrößen bis 32 Byte gesprochen haben. Andernfalls können Sie in die Mitte des Befehls springen und ein Problem mit dem Systemaufruf verursachen, der sich dort "verstecken" kann.

Regel C6 besagt, dass alle verfügbaren Anweisungen von Anfang an zerlegt werden können. Dies stellt somit sicher, dass wir jede Anweisung sehen und alle Anweisungen überprüfen können, die ausgeführt werden, wenn das Programm ausgeführt wird.

Regel C7 besagt, dass alle direkten Sprünge korrekt sind. Sie springen beispielsweise direkt zu dem Teil der Anweisung, in dem das Ziel angegeben ist, und obwohl es kein Vielfaches von 32 ist, ist es immer noch die richtige Anweisung, auf die die Demontage von links nach rechts angewendet wird.



Teilnehmerin: Was ist der Unterschied zwischen C5 und C3 ?

Professor: Ich denke, C5 sagt, dass ein Multibyte-Befehl die Grenzen benachbarter Adressen nicht überschreiten kann. Angenommen, ich habe einen Befehlsstrom und es gibt eine Adresse 32 und eine Adresse 64. Ein Befehl kann also das Grenzmultiplikator von 32 Bytes nicht überschreiten, dh er darf nicht mit einer Adresse kleiner als 64 beginnen und mit einer Adresse größer als 64 enden.



Dies ist, was Regel C5 sagt. Denn andernfalls können Sie nach einem Sprung der Multiplizität 32 in die Mitte eines anderen Befehls gelangen, bei dem nicht bekannt ist, was passiert.

Und Regel C3 ist ein Analogon zu diesem Verbot auf der Seite des Sprunges. Es besagt, dass bei jedem Sprung die Länge Ihres Sprungs ein Vielfaches von 32 sein muss.

C5 behauptet auch, dass alles im Adressbereich, das ein Vielfaches von 32 ist, eine sichere Anweisung ist.

Nachdem ich die Liste dieser Regeln gelesen hatte, hatte ich ein gemischtes Gefühl, da ich nicht beurteilen konnte, ob diese Regeln ausreichend sind, dh die Liste ist minimal oder vollständig.
Denken wir also über die Hausaufgaben nach, die Sie erledigen müssen. Ich denke, dass tatsächlich ein Fehler im Betrieb des nativen Clients vorliegt, wenn einige komplizierte Anweisungen in der Sandbox ausgeführt werden. Ich glaube, dass sie nicht die richtige Längencodierung hatten, was zu etwas Schlechtem führen könnte, aber ich kann mich nicht genau erinnern, was der Fehler war.

Angenommen, ein Sandbox-Validator erhält fälschlicherweise die Länge einer Anweisung. Was kann in diesem Fall passieren? Wie würden Sie diesen Slip verwenden?

Zielgruppe: Sie können beispielsweise den Systemaufruf oder die return-Anweisung ret ausblenden.

Professor: Ja. Angenommen, es gibt eine ausgefallene Version der AND- Anweisung, die Sie aufgeschrieben haben. Es ist möglich, dass der Validator sich geirrt hat und angenommen hat, dass seine Länge 6 Bytes mit der tatsächlichen Länge von 5 Bytes beträgt.



Was wird passieren? Der Validator betrachtet die Länge dieses Befehls als 6 Bytes und hat einen anderen gültigen Befehl dahinter. Der Prozessor verwendet jedoch beim Starten des Codes die tatsächliche Länge des Befehls, d. H. 5 Bytes. Infolgedessen haben wir am Ende der AND- Anweisung ein freies Byte, in das wir einen Systemaufruf einfügen und zu unserem Vorteil verwenden können. Und wenn wir hier ein CD- Byte einlegen, ist dies wie der Beginn einer anderen Anweisung. Als nächstes werden wir etwas in die nächste 6-Byte-Spanne einfügen, und es wird wie eine Anweisung aussehen, die mit dem CD- Byte beginnt, obwohl es tatsächlich Teil der AND- Anweisung ist. Danach können wir einen Systemaufruf tätigen und aus der Sandbox "entkommen".

Daher muss der Native Client- Validator seine Aktionen mit den Aktionen der CPU synchronisieren, dh genau erraten, wie der Prozessor jeden Befehl interpretiert. Und dies sollte auf jeder Ebene der Sandbox sein, was ziemlich schwierig zu implementieren ist.

In der Tat gibt es andere interessante Fehler im Native Client . Eine davon ist die falsche Bereinigung der Prozessorumgebung beim Sprung in die Trusted Service Runtime . Ich denke, wir werden gleich darüber sprechen. Die Trusted Service Runtime funktioniert jedoch grundsätzlich mit denselben CPU- Registern, mit denen nicht vertrauenswürdige Module ausgeführt werden können. Wenn der Prozessor vergisst, etwas zu löschen oder neu zu starten, kann die Laufzeit dadurch beeinträchtigt werden, dass das unzuverlässige Modul als vertrauenswürdige Anwendung betrachtet wird und etwas getan wird, das nicht hätte getan werden sollen oder das nicht die Absicht der Entwickler war.

Wo sind wir jetzt? Im Moment verstehen wir, wie man alle Anweisungen zerlegt und wie man die Ausführung verbotener Anweisungen verhindert. Nun wollen wir sehen, wie wir Speicher und Links für Code und Daten im Native Client- Modul speichern.

Aus Leistungsgründen beginnen die Native Client- Mitarbeiter, Hardware-Unterstützung zu verwenden, um sicherzustellen, dass das Speichern von Speicher und Links nicht wirklich viel Aufwand verursacht. Bevor ich jedoch über die von ihnen verwendete Hardwareunterstützung nachdenke, möchte ich Vorschläge hören. Wie könnte ich dasselbe ohne die Hardwareunterstützung tun? Können wir nur Zugriff auf alle Speicherprozesse innerhalb der zuvor von der Maschine festgelegten Grenzen gewähren?

Zielgruppe: Sie können Anweisungen instrumentieren, um alle hohen Bits zu löschen.



Professor: Ja, das ist richtig. Tatsächlich sehen wir, dass wir diese UND- Anweisung hier haben, und jedes Mal, wenn wir zum Beispiel irgendwohin springen, werden die niedrigen Bits gelöscht. Wenn wir jedoch den gesamten möglichen Code behalten möchten, der innerhalb der niedrigen 256 MB ausgeführt wird, können wir einfach das erste Attribut f durch 0 ersetzen und $ 0x0fffffe0 anstelle von $ 0xffffffe0 erhalten . Dies löscht die unteren Bits und legt eine Obergrenze von 256 MB fest.

Dies macht also genau das, was Sie anbieten, und stellt sicher, dass Sie sich bei jedem Sprung innerhalb von 256 MB befinden. Und die Tatsache, dass wir die Demontage durchführen, ermöglicht es auch zu überprüfen, ob alle direkten Sprünge in Reichweite sind.

Der Grund, warum sie dies für ihren Code nicht tun, ist, dass Sie auf der x86- Plattform AND sehr effektiv codieren können, wobei alle oberen Bits 1 sind. Dies führt zur Existenz eines 3-Byte-Befehls für AND und eines 2-Byte-Befehls für den Sprung. Somit haben wir einen zusätzlichen Aufwand von 3 Bytes. Wenn Sie jedoch ein High-Bit ohne Einheit benötigen, wie diese 0 anstelle von f , haben Sie plötzlich einen 5-Byte-Befehl. Daher denke ich, dass sie sich in diesem Fall Sorgen um den Overhead machen.

Zielgruppe: Gibt es ein Problem mit der Existenz einiger Anweisungen, die die Version erhöhen, die Sie erhalten möchten? Das heißt, Sie können sagen, dass Ihre Anweisung eine konstante Tendenz oder so etwas haben kann?

Professor: Ich denke schon. Sie werden wahrscheinlich Anweisungen verbieten, die zu einer komplexen Adressformel springen, und nur Anweisungen unterstützen, die direkt zu diesem Wert springen, und dieser Wert erhält immer AND .

Zielgruppe: Für den Zugriff auf den Speicher ist mehr erforderlich als ...

Professor: Ja, weil es nur Code ist. Und für den Zugriff auf Speicher auf der x86- Plattform gibt es viele seltsame Möglichkeiten, auf einen bestimmten Speicherort zuzugreifen. Normalerweise müssen Sie zuerst den Speicherort berechnen, dann ein zusätzliches UND hinzufügen und erst dann darauf zugreifen. Ich denke, dies ist der wahre Grund für ihre Besorgnis über den Leistungsabfall aufgrund der Verwendung dieses Toolkits.

Auf der x86- Plattform oder zumindest auf der im Artikel beschriebenen 32-Bit-Plattform verwenden sie Hardwareunterstützung, anstatt die Code- und Adressdaten einzuschränken, die auf nicht vertrauenswürdige Module verweisen.

Lassen Sie uns sehen, wie es aussieht, bevor wir herausfinden, wie das NaCl- Modul in einer Sandbox verwendet wird. Diese Hardware wird als Segmentierung bezeichnet. Es entstand noch bevor die x86- Plattform eine Auslagerungsdatei bekam. Auf der x86- Plattform ist während des Vorgangs eine unterstützte Hardwaretabelle vorhanden. Wir nennen es die Tabelle der Segmentdeskriptoren. Es handelt sich um eine Reihe von Segmenten, die von 0 bis zum Ende einer Tabelle beliebiger Größe nummeriert sind. Dies ist so etwas wie ein Dateideskriptor unter Unix , außer dass jeder Eintrag aus zwei Werten besteht: der Basisbasis und der Länge.

Diese Tabelle sagt uns, dass wir ein Paar von Segmenten haben, und jedes Mal, wenn wir uns auf ein bestimmtes Segment beziehen, bedeutet dies in gewissem Sinne, dass es sich um ein Stück Speicher handelt, das an der Basisadresse der Basis beginnt und sich über die Länge erstreckt .



Dies hilft uns, die Speichergrenzen auf der x86- Plattform beizubehalten , da jeder Befehl, der auf den Speicher zugreift, auf ein bestimmtes Segment in dieser Tabelle verweist.

Wenn wir beispielsweise mov (% eax) (% ebx) ausführen , dh den Speicherwert von einem im EAX- Register gespeicherten Zeiger auf einen anderen im EBX- Register gespeicherten Zeiger verschieben, weiß das Programm, wie die Start- und Endadressen lauten im Hinblick auf und speichert den Wert in der zweiten Adresse.

Tatsächlich gibt es auf der x86- Plattform, wenn wir über Speicher sprechen, eine implizite Sache, die als Segmentdeskriptor bezeichnet wird, ähnlich einem Dateideskriptor in Unix . Dies ist nur ein Index in der Deskriptortabelle. Sofern nicht anders angegeben, enthält jeder Operationscode ein Standardsegment.

Wenn Sie mov (% eax) ausführen, bezieht sich dies daher auf % ds oder auf das Datensegmentregister, das ein spezielles Register in Ihrem Prozessor ist. Wenn ich mich richtig erinnere, ist es eine 16-Bit-Ganzzahl, die auf diese Deskriptortabelle verweist.

Das Gleiche gilt für (% ebx) - es bezieht sich auf denselben % ds- Segmentselektor. Tatsächlich haben wir in x86 eine Gruppe von 6 Code-Selektoren: CS, DS, ES, FS, GS und SS . Der CS-Anrufwähler wird implizit zum Empfangen von Anweisungen verwendet. Wenn Ihr Anweisungszeiger also auf etwas zeigt, bezieht er sich auf denjenigen, der den CS- Segment-Selektor ausgewählt hat.



Die meisten Datenreferenzen verwenden implizit DS oder ES , FS und GS weisen auf einige besondere Dinge hin, und SS wird immer für Stapeloperationen verwendet. Und wenn Sie Push & Pop ausführen, stammen diese implizit aus dieser Segmentauswahl. Dies ist eine ziemlich archaische Mechanik, die sich jedoch in diesem speziellen Fall als äußerst nützlich herausstellt.

Wenn Sie beispielsweise im Selektor % ds: addr Zugriff auf eine Adresse erhalten, leitet die Hardware diese mit der Tabelle adrr + T [% ds] .base an die Operation weiter . Dies bedeutet, dass die Adresslänge des Moduls aus derselben Tabelle stammt. Jedes Mal, wenn Sie auf den Speicher zugreifen, verfügt er über eine Datenbank mit Segmentselektoren in Form von Deskriptortabelleneinträgen. Die von Ihnen angegebene Adresse wird mit der Länge des entsprechenden Segments abgeglichen.

Zielgruppe: Warum wird es beispielsweise nicht zum Schutz des Puffers verwendet?

Professor: Ja, das ist eine gute Frage! Könnten wir dies zum Schutz vor Pufferüberläufen verwenden? Zum Beispiel können Sie für jeden Puffer, den wir haben, die Pufferbasis hier und dort die Puffergröße einfügen.

Teilnehmerin: Was ist, wenn Sie es nicht in eine Tabelle legen müssen, bevor Sie es schreiben möchten? Sie müssen nicht ständig da sein.

Professor: Ja. Daher denke ich, dass der Grund dafür, dass dieser Ansatz nicht oft zum Schutz vor Pufferüberläufen verwendet wird, darin besteht, dass die Anzahl der Datensätze in dieser Tabelle 2 in der 16. Potenz nicht überschreiten kann, da Deskriptoren 16 Bit lang sind, aber tatsächlich Tatsächlich werden ein paar weitere Bits für andere Dinge verwendet. Tatsächlich können Sie in dieser Tabelle nur 2 in der 13. Potenz der Datensätze platzieren. Wenn Sie in Ihrem Code ein Datenarray haben, das größer als 2 13 ist , kann es daher zu einem Überlauf dieser Tabelle kommen.

Darüber hinaus wäre es für den Compiler seltsam, diese Tabelle direkt zu verwalten, da sie normalerweise mithilfe von Systemaufrufen bearbeitet wird. Sie können nicht direkt in diese Tabelle schreiben. Zuerst müssen Sie einen Systemaufruf an das Betriebssystem senden. Danach legt das Betriebssystem den Datensatz in dieser Tabelle ab. Daher denke ich, dass die meisten Compiler einfach nicht mit einem so komplexen Speicherpuffer-Managementsystem umgehen wollen.



Übrigens verwendet Multex diesen Ansatz: Es verfügt über 2 18 Datensätze für verschiedene Segmente und 2 18 Datensätze für mögliche Offsets. Und jedes gemeinsame Bibliotheksfragment oder Speicherfragment sind separate Segmente. Sie werden alle auf Reichweite geprüft und können daher nicht auf variabler Ebene verwendet werden.

Zielgruppe: Vermutlich verlangsamt die ständige Notwendigkeit, den Kernel zu verwenden, den Prozess.

Professor: Ja, das ist richtig. Wir haben also Overhead aufgrund der Tatsache, dass wir, wenn plötzlich ein neuer Puffer auf dem Stapel erstellt wird, einen Systemaufruf ausführen müssen, um ihn hinzuzufügen.

Wie viele dieser Elemente verwenden tatsächlich den Segmentierungsmechanismus? Sie können sich vorstellen, wie es funktioniert. Ich denke, standardmäßig haben alle diese Segmente in x86 eine Basis gleich 0 und die Länge liegt zwischen 2 und 32. Auf diese Weise können Sie auf den gesamten gewünschten Speicherbereich zugreifen. Daher codieren sie für NaCl die Basis 0 und setzen die Länge auf 256 Megabyte. Dann zeigen sie auf alle Register von 6 Segmentselektoren in diesem Datensatz für den 256-MB-Bereich. Wenn das Gerät auf den Speicher zugreift, ändert es ihn mit einem Offset von 256 MB. Die Möglichkeit, das Modul zu ändern, ist daher auf 256 MB beschränkt.

Ich denke, Sie verstehen jetzt, wie diese Hardware unterstützt wird und wie sie funktioniert, sodass Sie möglicherweise diese Segmentselektoren verwenden.
Was kann also schief gehen, wenn wir diesen Plan nur umsetzen? Können wir in einem nicht vertrauenswürdigen Modul aus der Segmentauswahl herausspringen? Ich denke, eine Sache, mit der man vorsichtig sein muss, ist, dass diese Register wie reguläre Register sind und man Werte in sie hinein und aus ihnen heraus verschieben kann. Daher müssen Sie sicherstellen, dass das nicht vertrauenswürdige Modul diese Segmentauswahlregister nicht verzerrt. Denn irgendwo in der Deskriptortabelle befindet sich möglicherweise ein Datensatz, der auch der Quellensegmentdeskriptor für einen Prozess ist, der eine Basis von 0 und eine Länge von bis zu 2 32 hat .



Wenn also ein unzuverlässiges Modul CS , DS oder ES oder einen dieser Selektoren so ändern konnte, dass sie auf dieses ursprüngliche Betriebssystem verweisen, das Ihren gesamten Adressraum abdeckt, können Sie eine Speicherverknüpfung zu diesem Segment herstellen und " aus dem Sandkasten springen.

Daher musste der native Client dieser verbotenen Liste einige weitere Anweisungen hinzufügen. Ich denke, dass sie alle Anweisungen wie mov% ds, es und so weiter verbieten. Daher können Sie in der Sandbox nicht das Segment ändern, auf das sich einige Dinge beziehen, die sich darauf beziehen. Auf der x86- Plattform sind Anweisungen zum Ändern der Segmentdeskriptortabelle privilegiert, aber das Ändern der ds, es selbst usw. Der Tisch ist völlig unprivilegiert.

Zielgruppe: Können Sie die Tabelle so initialisieren, dass in allen nicht verwendeten Slots die Länge Null platziert wird?

Professor: Ja. Sie können die Tabellenlänge für etwas festlegen, bei dem keine nicht verwendeten Slots vorhanden sind. Es stellt sich heraus, dass Sie diesen zusätzlichen Steckplatz mit 0 und 2 32 wirklich benötigen, da die vertrauenswürdige Laufzeitumgebung in diesem Segment beginnen und Zugriff auf den gesamten Speicherbereich erhalten sollte. Dieser Eintrag ist also erforderlich, damit die vertrauenswürdige Laufzeitumgebung funktioniert.

Zielgruppe: Was wird benötigt, um die Länge der Ausgabe der Tabelle zu ändern?
Professor: Sie müssen Root-Rechte haben. Linux hat tatsächlich ein System namens modify_ldt () für die lokale Deskriptortabelle, mit dem jeder Prozess seine eigene Tabelle ändern kann, dh es gibt tatsächlich eine Tabelle für jeden Prozess. Auf der x86- Plattform ist dies jedoch komplizierter. Es gibt sowohl eine globale als auch eine lokale Tabelle. Eine lokale Tabelle für einen bestimmten Prozess kann geändert werden.

Versuchen wir nun herauszufinden, wie wir vom Native Client- Ausführungsprozess springen oder springen oder aus der Sandbox springen. Was bedeutet es, aus uns herauszuspringen?



Wir müssen also diesen vertrauenswürdigen Code ausführen, und dieser vertrauenswürdige Code "lebt" irgendwo über der Grenze von 256 MB. Um dorthin zu gelangen, müssen wir alle vom Native Client installierten Schutzfunktionen rückgängig machen. Grundsätzlich kommt es darauf an, diese sechs Selektoren zu ändern. Ich denke, dass unser Validator nicht die gleichen Regeln für Dinge anwenden wird, die sich oberhalb der 256-MB-Grenze befinden, daher ist dies recht einfach.

Aber dann müssen wir irgendwie in die vertrauenswürdige Laufzeit-Laufzeit springen und die Segment-Selektoren auf die richtigen Werte für dieses riesige Segment neu installieren, wobei der Adressraum des gesamten Prozesses abgedeckt wird - dieser Bereich liegt zwischen 0 und 2 32 . Sie nannten solche Mechanismen, die in den Native Client- Trampolinen und Trittbrettern vorhanden sind . Sie leben in einem niedrigen 64k-Modul. Das Coolste ist, dass diese „Trampoline“ und „Sprünge“ Codeteile sind, die in den unteren 64 KB des Prozessraums liegen. Dies bedeutet, dass dieses unzuverlässige Modul dorthin springen kann, da es sich um eine gültige Codeadresse handelt, die innerhalb der Grenzen von 32 Bit und innerhalb von 256 MB liegt. Sie können also auf dieses Trampolin springen.

Native Client «» - . , Native Client «», trampoline trusted runtime . , DS, CS , .

, , - malo , «», «» 32- .

, 4096 + 32 , . , , mov %ds, 7 , ds , 7 0 2 32 . CS trusted service runtime , 256 .



, , , trusted service runtime , . , . DS , , , , - .

, ? , «»? , ?

: 64.

: , , . malo, 64, 32 . , , , .

, 32- , . , , 32 , 32- , . «» trusted runtime 32 .



. , , DS, CS . , 256- , trusted runtime , . .

«», trusted runtime 256 Native Client . «» DS , , mov %ds, 7 , , trusted runtime . . , «», - .

halt 32- «». «», . trusted service runtime , 1 .



trusted service runtime , , .

: «» ?

: «» 0 256 . 64- , , «», - -. Native Client .

: ?

: , ? , «»? ?

: , ?

: , - %eax , trusted runtime : «, »! EAX , mov , «» EAX , trusted runtime . , «»?

: , , . …

: , , — , , 0 2 32 . . «», 256 .

, «», . , «» , . , «» .

: «» 256 ?

: , . , CS - . «», halt , mov, CS , , 256 .

, , «». , DS , , CSund irgendwo springen.

Wenn Sie es versuchen würden, könnten Sie wahrscheinlich eine Folge von x86- Anweisungen entwickeln , die dies außerhalb der Grenzen des Adressraums des Native Client- Moduls tun könnten .

Wir sehen uns also nächste Woche und sprechen über Web-Sicherheit.


Die Vollversion des Kurses finden Sie hier .

Vielen Dank für Ihren Aufenthalt bei uns. Gefällt dir unser Artikel? Möchten Sie weitere interessante Materialien sehen? Unterstützen Sie uns, indem Sie eine Bestellung aufgeben oder sie Ihren Freunden empfehlen. Habr-Benutzer erhalten 30% Rabatt auf ein einzigartiges Analogon von Einstiegsservern, das wir für Sie erfunden haben: Die ganze Wahrheit über VPS (KVM) E5-2650 v4 (6 Kerne) 10 GB DDR4 240 GB SSD 1 Gbit / s $ 20 oder wie teilt man den Server? (Optionen sind mit RAID1 und RAID10, bis zu 24 Kernen und bis zu 40 GB DDR4 verfügbar).

VPS (KVM) E5-2650 v4 (6 Kerne) 10 GB DDR4 240 GB SSD 1 Gbit / s bis Dezember kostenlos, wenn Sie für einen Zeitraum von sechs Monaten bezahlen, können Sie hier bestellen .

Dell R730xd 2 mal günstiger? Nur wir haben 2 x Intel Dodeca-Core Xeon E5-2650v4 128 GB DDR4 6 x 480 GB SSD 1 Gbit / s 100 TV von 249 US-Dollar in den Niederlanden und den USA! Lesen Sie mehr über den Aufbau eines Infrastrukturgebäudes. Klasse mit Dell R730xd E5-2650 v4 Servern für 9.000 Euro für einen Cent?

Source: https://habr.com/ru/post/de418227/


All Articles