Geschichte, wie man eine Zeitmaschine für eine Datenbank erstellt und versehentlich einen Exploit schreibt

Guten Tag, Habr.

Haben Sie sich jemals gefragt, wie Sie die Zeit in der Datenbank ändern können? Einfach? In einigen Fällen ist es einfach - der Linux-Befehl ist Datum und der Punkt ist im Hut. Und wenn Sie die Zeit nur innerhalb einer Instanz der Datenbank ändern müssen, wenn sich mehrere davon auf dem Server befinden? Und für einen einzelnen Datenbankprozess? UND? Äh, das ist es, mein Freund, das ist der springende Punkt.Jemand wird sagen, dass dies ein weiterer Sur ist, der nicht mit der Realität zusammenhängt und regelmäßig auf Habré ausgelegt wird. Aber nein, die Aufgabe ist ziemlich real und wird von der Notwendigkeit der Produktion bestimmt - dem Testen von Code. Obwohl ich damit einverstanden bin, kann der Testfall ziemlich exotisch sein - überprüfen Sie, wie sich der Code für ein bestimmtes Datum in der Zukunft verhält. In diesem Artikel werde ich im Detail untersuchen, wie diese Aufgabe gelöst wurde, und gleichzeitig ein wenig den Prozess der Organisation von Test- und Entwickler-Ständen für die Oracle-Basis erfassen. Machen Sie es sich vor einer langen Lektüre bequem und fragen Sie nach einer Katze.

Hintergrund


Beginnen wir mit einer kurzen Einführung, um zu zeigen, warum dies notwendig ist. Wie bereits angekündigt, schreiben wir Tests bei der Implementierung von Änderungen in der Datenbank. Das System, unter dem diese Tests durchgeführt werden, wurde zu Beginn (oder kurz vor dem Start) der Nullen entwickelt, sodass sich die gesamte Geschäftslogik in der Datenbank befindet und in Form von gespeicherten Prozeduren in der Sprache pl / sql geschrieben ist. Und ja, es bringt uns Schmerz und Leid. Aber das ist Vermächtnis, und man muss damit leben. Im Code und im Tabellenmodell kann angegeben werden, wie sich die Parameter im System im Laufe der Zeit entwickeln. Mit anderen Worten, legen Sie die Aktivität fest, ab welchem ​​Datum und bis zu welchem ​​Datum sie angewendet werden können. Was ist weit zu gehen? Die jüngste Änderung des Mehrwertsteuersatzes ist ein anschauliches Beispiel dafür. Und damit solche Änderungen im System vorab überprüft werden können,Eine Datenbank mit solchen Änderungen muss zu einem bestimmten Zeitpunkt in der Zukunft übertragen werden. Die Codeparameter in den Tabellen werden zum "aktuellen Zeitpunkt" aktiv. Aufgrund der Besonderheiten des unterstützten Systems können Sie keine Mock-Tests verwenden, bei denen der Rückgabewert des aktuellen Systemdatums in der Sprache zu Beginn der Testsitzung einfach geändert wird.

Also haben wir bestimmt warum, dann müssen wir bestimmen, wie das Ziel erreicht wird. Zu diesem Zweck werde ich einen kleinen Rückblick auf die Optionen zum Erstellen von Testbänken für Entwickler und den Beginn jeder Testsitzung geben.

Steinzeit


Es war einmal, als die Bäume klein und die Mainframes groß waren, gab es nur einen Server für die Entwicklung und es wurden auch Tests durchgeführt. Und im Prinzip war dies alles für alle ausreichend ( 640 KB sind für alle ausreichend! )

Nachteile: Um die Aufgabe der Zeitänderung zu erfüllen, mussten viele verwandte Abteilungen einbezogen werden - Systemadministratoren (hat sich die Zeit auf dem Subserver von root geändert), DBMS-Administratoren (hat die Datenbank neu gestartet), Programmierer ( Es musste mitgeteilt werden, dass eine Zeitänderung eintreten würde, da ein Teil des Codes nicht mehr funktioniert. Beispielsweise waren Web-Token, die zuvor für den Aufruf von API-Methoden ausgegeben wurden, nicht mehr gültig und dies könnte eine Überraschung sein. Tester (testen sich selbst) ... Wenn Sie die Zeit auf die Gegenwart zurückführen alles wurde in umgekehrter Reihenfolge wiederholt.

Mittelalter


Mit der Zeit wuchs die Anzahl der Entwickler in der Abteilung und irgendwann reichte 1 Server nicht mehr aus. Hauptsächlich aufgrund der Tatsache, dass verschiedene Entwickler dasselbe pl / sql-Paket ändern und Tests dafür durchführen möchten (auch ohne die Zeit zu ändern). Immer mehr Empörung war zu hören: „Wie lange! Genug, das zu tolerieren! Fabriken für Arbeiter, Land für Bauern! Jeder Programmierer hat eine Datenbank! “ Wenn Sie jedoch über ein paar Terabyte Produktdatenbank und 50 bis 100 Entwickler verfügen, ist die Anforderung in dieser Form ehrlich gesagt nicht sehr real. Und dennoch möchte jeder, dass die Test- und Entwicklerbasis nicht sehr hinter den Verkäufen zurückbleibt, sowohl in der Struktur als auch in den Daten in den Tabellen. Es gab also einen separaten Server zum Testen, nennen wir es Vorproduktion. Es wurde von 2 identischen Servern gebaut,wo der Verkauf gemacht wurde, um die Datenbank von RMAN Dollar wiederherzustellen, und es dauerte etwa 2-2,5 Tage. Nach der Wiederherstellung wurden in der Datenbank persönliche und andere wichtige Daten anonymisiert, und die Last aus den Testanwendungen wurde auf diesen Server angewendet (und die Programmierer selbst arbeiteten immer mit dem kürzlich wiederhergestellten Server). Die Arbeit mit dem erforderlichen Server wurde mithilfe der von corosync (Schrittmacher) unterstützten Cluster-IP-Ressource sichergestellt. Während alle mit dem aktiven Server arbeiten, beginnt auf dem 2. Knoten die Datenbankwiederherstellung erneut und nach 2-3 Tagen wechseln sie erneut die Plätze.Die Arbeit mit dem erforderlichen Server wurde mithilfe der von corosync (Schrittmacher) unterstützten Cluster-IP-Ressource sichergestellt. Während alle mit dem aktiven Server arbeiten, beginnt auf dem 2. Knoten die Datenbankwiederherstellung erneut und nach 2-3 Tagen wechseln sie erneut die Plätze.Die Arbeit mit dem erforderlichen Server wurde mithilfe der von corosync (Schrittmacher) unterstützten Cluster-IP-Ressource sichergestellt. Während alle mit dem aktiven Server arbeiten, beginnt auf dem 2. Knoten die Datenbankwiederherstellung erneut und nach 2-3 Tagen wechseln sie erneut die Plätze.

Von den offensichtlichen Nachteilen: Sie benötigen 2 Server und 2 mal mehr Ressourcen (hauptsächlich Festplatte) als prod.

Vorteile: Zeitänderungsbetrieb und -tests - können auf dem 2. Server ausgeführt werden, auf dem Hauptserver zu diesem Zeitpunkt, an dem Entwickler leben und ihren Geschäften nachgehen. Serverwechsel treten nur auf, wenn die Datenbank bereit ist und die Ausfallzeit der Testumgebung minimal ist.

Die Ära des wissenschaftlichen und technologischen Fortschritts


Als wir zur 11g Release 2-Datenbank wechselten, lasen wir über eine interessante Technologie, die Oracle unter dem Namen CloneDB bereitstellt. Das Fazit ist, dass die Produktdatenbanksicherungen (es gibt eine direkte Bitkopie der Produktdatendateien) auf einem speziellen Server gespeichert sind, der diesen Datensatz dann über DNFS (direktes NFS) auf einer beliebigen Anzahl von Servern veröffentlicht, und Sie müssen keinen auf dem Server haben Dieselbe Datenträgermenge, da der Copy-On-Write-Ansatz implementiert ist: Die Datenbank verwendet eine Netzwerkfreigabe mit Datendateien vom Sicherungsserver zum Lesen von Daten in Tabellen, und Änderungen werden in lokale Datendateien auf dem Entwicklungsserver selbst geschrieben. In regelmäßigen Abständen wird für den Server das „Nullstellen der Fristen“ durchgeführt, damit die lokalen Datendateien nicht sehr stark wachsen und der Ort nicht endet. Beim Aktualisieren des Servers werden die Daten auch in den Tabellen entpersönlicht.In diesem Fall fallen alle Tabellenaktualisierungen in lokale Datendateien und diese Tabellen werden vom lokalen Server gelesen, alle anderen Tabellen werden über das Netzwerk gelesen.

Nachteile: Es gibt immer noch 2 Server (um reibungslose Updates mit minimalen Ausfallzeiten für Verbraucher zu gewährleisten), aber jetzt wird das Festplattenvolumen erheblich reduziert. Um Geld auf einem NFS-Ball zu speichern, benötigen Sie 1 weiteren Server in Größe + - als Produkt, aber die Ausführungszeit für das Update selbst wird reduziert (insbesondere bei Verwendung von inkrementellen Dollars). Die Vernetzung mit einem NFS-Ball verlangsamt die E / A-Lesevorgänge spürbar. Um die CloneDB-Technologie verwenden zu können, muss die Basis eine Enterprise Edition sein. In unserem Fall mussten wir das Upgrade-Verfahren jedes Mal auf Testbasen durchführen. Glücklicherweise sind Testdatenbanken von den Oracle-Lizenzrichtlinien ausgenommen.

Vorteile: Der Vorgang zum Wiederherstellen einer Basis aus einem Bakup dauert weniger als 1 Tag (ich erinnere mich nicht an die genaue Zeit).

Zeitumstellung: keine wesentlichen Änderungen. Obwohl zu diesem Zeitpunkt bereits Skripte erstellt wurden, um die Zeit auf dem Server zu ändern und die Datenbank neu zu starten, um dies zu tun, ohne die Aufmerksamkeit der Pfleger der Administratoren auf sich zu ziehen.

Ära der neuen Geschichte


Um noch mehr Speicherplatz zu sparen und Daten offline zu lesen, haben wir uns entschlossen, unsere CloneDB-Version (mit Flashback und Snapshots) mithilfe eines Dateisystems mit Komprimierung zu implementieren. Während der Vorversuche fiel die Wahl auf ZFS, obwohl es im Linux-Kernel keine offizielle Unterstützung dafür gibt (Zitat aus dem Artikel) Zum Vergleich haben wir uns auch BTRFS (b-tree fs) angesehen, das Oracle fördert, aber das Komprimierungsverhältnis war bei gleichem CPU- und RAM-Verbrauch in den Tests geringer. Um die ZFS-Unterstützung für RHEL5 zu aktivieren, wurde ein eigener Kernel basierend auf UEK (unzerbrechlicher Unternehmenskernel) erstellt. Auf neueren Achsen und Kerneln können Sie einfach den vorgefertigten UEK-Kernel verwenden. Die Implementierung einer solchen Testbasis basiert ebenfalls auf dem COW-Mechanismus, jedoch auf der Ebene von Dateisystem-Snapshots. Dem Server werden 2 Festplattengeräte zur Verfügung gestellt, auf einem wird der zfs-Pool erstellt, wobei über RMAN eine zusätzliche Standby-Datenbank aus dem Verkauf erstellt wird. Da wir die Komprimierung verwenden, nimmt die Partition weniger als die Produktion in Anspruch.
Das System ist auf dem zweiten Festplattengerät installiert, und der Rest ist erforderlich, damit der Server und die Datenbank selbst funktionieren, z. B. Partitionen zum Rückgängigmachen und Temp. Sie können jederzeit einen Snapshot aus dem zfs-Pool erstellen, der dann als separate Datenbank geöffnet wird. Das Erstellen eines Schnappschusses dauert einige Sekunden. Es ist Magie! Und solche Datenbanken können im Prinzip ziemlich stark gekippt werden, wenn nur der Server über genügend RAM für alle Instanzen und die Größe des zfs-Pools selbst verfügt (zum Speichern von Änderungen in Datendateien während der Depersonalisierung und während des Lebenszyklus des Datenbankklons). Die Hauptzeit für die Aktualisierung der Testbasis ist der Vorgang der Datendepersonalisierung, sie passt jedoch auch in 15 bis 20 Minuten. Es gibt eine signifikante Beschleunigung.

Nachteile: Auf dem Server können Sie die Zeit nicht einfach durch Übersetzen der Systemzeit ändern, da dann alle auf diesem Server ausgeführten Datenbankinstanzen sofort in diese Zeit fallen. Eine Lösung für dieses Problem wurde gefunden und wird im entsprechenden Abschnitt beschrieben. Mit Blick auf die Zukunft möchte ich sagen, dass Sie damit die Zeit in nur einer Instanz der Datenbank ändern können (Ansatz zur Änderung der Zeit pro Instanz)) ohne den Rest auf demselben Server zu beeinflussen. Und die Zeit auf dem Server selbst ändert sich auch nicht. Dadurch ist kein Root-Skript erforderlich, um die Zeit auf dem Server zu ändern. Ebenfalls in dieser Phase wird die Zeitänderungsautomatisierung für Instanzen über Jenkins CI implementiert, und Benutzer (relativ gesehen Entwicklungsteams), die ihren Stand besitzen, erhalten Rechte an den Jobs, über die sie selbst die Zeit ändern, den Stand mit Verkäufen auf den aktuellen Status aktualisieren und Schnappschüsse erstellen können und Wiederherstellung (Rollback) der Basis zum zuvor erstellten Snapshot.

Ära der jüngeren Geschichte


Mit dem Aufkommen von Oracle 12c erschien eine neue Technologie - steckbare Datenbanken und damit Container-Datenbanken (cdb). Mit dieser Technologie können innerhalb einer physischen Instanz mehrere "virtuelle" Datenbanken erstellt werden, die sich einen gemeinsamen Speicherbereich der Instanz teilen. Vorteile: Sie können Speicher für den Server speichern (und die Gesamtleistung unserer Datenbank steigern, da der gesamte Speicher, der zuvor belegt war, z. B. 5 verschiedene Instanzen, für alle bereitgestellten PDF-Container in CDB freigegeben werden kann und nur von diesen verwendet wird Wenn sie es wirklich brauchen und nicht wie in der vorherigen Phase, wenn jede Instanz den ihr zugewiesenen Speicher für sich selbst "blockiert" und wenn die Aktivität eines der Klone gering war, wurde der Speicher nicht effektiv genutzt, mit anderen Worten, er war inaktiv.Die Datendateien verschiedener pdb befinden sich immer noch im zfs-Pool, und beim Bereitstellen von Klonen verwenden sie denselben zfs-Snapshot-Mech. Zu diesem Zeitpunkt kamen wir der Fähigkeit nahe, fast jedem Entwickler eine eigene Datenbank zur Verfügung zu stellen. Das Ändern der Zeit in dieser Phase erfordert keinen Neustart der Datenbank und funktioniert nur für Prozesse, die eine Zeitänderung erfordern, sehr genau. Alle anderen Benutzer, die mit dieser Datenbank arbeiten, sind in keiner Weise betroffen.

Minuspunkt: Sie können den Zeitänderungsansatz pro Instanz aus der vorherigen Phase nicht verwenden, da wir jetzt eine Instanz haben. Es wurde jedoch eine Lösung für diesen Fall gefunden. Und genau dies war der Anstoß für das Schreiben dieses Artikels. Mit Blick auf die Zukunft werde ich sagen, dass es sich um eine zeitliche Änderung pro Prozessansatz handelt , d. H. In jedem Datenbankprozess können Sie im Allgemeinen Ihre eigene eindeutige Zeit festlegen.

In diesem Fall legt eine typische Testsitzung unmittelbar nach dem Herstellen einer Verbindung zur Datenbank den richtigen Zeitpunkt zu Beginn ihrer Arbeit fest, führt Tests durch und gibt die Zeit am Ende zurück. Die Rückgabe der Zeit ist aus einem einfachen Grund erforderlich: Einige Oracle-Datenbankprozesse werden nicht beendet, wenn der Datenbankclient die Verbindung zum Server trennt. Hierbei handelt es sich um Serverprozesse, die als gemeinsam genutzte Server bezeichnet werden und im Gegensatz zu dedizierten Prozessen ausgeführt werden, wenn der Datenbankserver gestartet wird und nahezu unbegrenzt aktiv ist (im Idealfall) Bild der Welt). Wenn Sie die Zeit in einem solchen Serverprozess geändert lassen, erhält eine andere Verbindung, die in diesem Prozess bedient wird, die falsche Zeit.

In unserem System werden häufig gemeinsam genutzte Server verwendet, weil Bis zu 11 g gab es praktisch keine adäquate Lösung für unser System, um einer hohen Last standzuhalten (in 11 g erschien DRCP - datenbankresidentes Verbindungspooling). Und hier ist der Grund: In Sub gibt es eine Begrenzung für die Gesamtzahl der Serverprozesse, die sowohl im dedizierten als auch im gemeinsam genutzten Modus erstellt werden können. Dedizierte Prozesse werden langsamer erzeugt, als die Datenbank einen vorgefertigten gemeinsam genutzten Prozess aus dem Pool gemeinsam genutzter Prozesse ausgeben kann. Wenn also ständig neue Verbindungen eingehen (insbesondere wenn der Prozess andere langsame Vorgänge ausführt), wächst die Gesamtzahl der Prozesse. Wenn das Limit an Sitzungen / Prozessen erreicht ist, werden in der Datenbank keine neuen Verbindungen mehr hergestellt, und es kommt zu einem Zusammenbruch.Durch den Übergang zur Verwendung eines Pools gemeinsam genutzter Prozesse konnten wir die Anzahl neuer Prozesse auf dem Server beim Herstellen einer Verbindung reduzieren.

Hier ist die Überprüfung der Technologien zum Erstellen von Testdatenbanken abgeschlossen, und wir können endlich mit der Implementierung der Zeitänderungsalgorithmen für die Datenbank selbst beginnen.

Der Fake-per-Instance-Ansatz


Wie ändere ich die Zeit in der Datenbank?

Das erste, was mir in den Sinn kam, war, in einem Schema, das den gesamten Geschäftslogikcode enthält, eine eigene Funktion zu erstellen, die die mit der Zeit arbeitenden Sprachfunktionen (sysdate, current_date usw.) überlappt und unter bestimmten Bedingungen andere Werte angibt, z Legen Sie zu Beginn des Testlaufs Werte über den Sitzungskontext fest. Es hat nicht funktioniert, die integrierten Sprachfunktionen haben sich nicht mit den Benutzerfunktionen überschnitten.

Anschließend wurden leichte Virtualisierungssysteme (Vserver, OpenVZ) und die Containerisierung über Docker getestet. Es funktioniert auch nicht, sie verwenden denselben Kernel wie das Hostsystem, was bedeutet, dass sie dieselben Systemzeitgeberwerte verwenden. Wieder rausfallen.

Und hier habe ich keine Angst davor, dieses Wort zu retten, eine großartige Erfindung der Linux-Welt - Neudefinition / Abfangen von Funktionen in der Phase des dynamischen Ladens gemeinsam genutzter Objekte. Es ist vielen als Tricks mit LD_PRELOAD bekannt. In der Umgebungsvariablen LD_PRELOAD können Sie die Bibliothek angeben, die vor allen anderen geladen werden soll, die der Prozess benötigt. Wenn diese Bibliothek Zeichen mit demselben Namen wie beispielsweise in der Standardbibliothek enthält, die später geladen wird, sieht die Symbolimporttabelle für die Anwendung wie eine Funktion aus liefert unser Ersatzmodul. Und genau das macht die libfaketime- Projektbibliothekdie wir zu verwenden begannen, um die Datenbank zu einem anderen Zeitpunkt als dem System zu starten. Die Bibliothek verpasst Anrufe, die die Arbeit mit dem Systemzeitgeber und das Abrufen der Systemzeit und des Systemdatums betreffen. Um zu steuern, wie viel Zeit sich relativ zum aktuellen Serverdatum bewegt oder ab welchem ​​Zeitpunkt die Zeit innerhalb des Prozesses liegen soll, wird alles durch Umgebungsvariablen gesteuert, die zusammen mit LD_PRELOAD festgelegt werden müssen. Um die Zeitänderung zu implementieren, haben wir einen Job auf dem Jenkins-Server implementiert, der in den Datenbankserver eingeht und das DBMS entweder mit oder ohne für libfaketime festgelegte Umgebungsvariablen neu startet.

Ein Beispielalgorithmus zum Starten einer Datenbank mit einer Substitutionszeit:

export LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so
export FAKETIME="+1d"
export FAKETIME_NO_CACHE=1

$ORACLE_HOME/bin/sqlplus @/home/oracle/scripts/restart_db.sql

Und wenn Sie sich vorstellen, dass alles sofort funktioniert hat, dann irren Sie sich zutiefst. Wie sich herausstellte, werden die Bibliotheken überprüft, die beim Start des DBMS in den Prozess geladen werden. Und im Alarmprotokoll beginnt er, die bemerkte Fälschung zu ärgern, während die Basis nicht startet. Jetzt weiß ich nicht mehr genau, wie ich es loswerden soll. Es gibt einige Parameter, die die Ausführung von Sanity-Checks beim Start deaktivieren können.

Der Fake-per-Process-Ansatz


Die allgemeine Idee, die Zeit nur innerhalb eines Prozesses zu ändern, blieb dieselbe - verwenden Sie libfaketime. Wir starten die Datenbank mit einer vorinstallierten Bibliothek, setzen jedoch beim Start einen Zeitversatz von Null, der dann an alle DBMS-Prozesse weitergegeben wird. Legen Sie dann innerhalb der Testsitzung die Umgebungsvariable nur für diesen Prozess fest. Pff, etwas geschäftliches.

Für diejenigen, die mit der pl / sql-Sprache vertraut sind, ist das ganze Schicksal dieser Idee jedoch sofort klar. Weil die Sprache sehr begrenzt und grundsätzlich für hochrangige Aufgaben geeignet ist. Dort kann keine Systemprogrammierung implementiert werden. Obwohl einige Operationen auf niedriger Ebene (z. B. Arbeiten mit einem Netzwerk, Arbeiten mit Dateien) in Form von vorinstallierten System-dbms / utl-Paketen vorhanden sind. Während der gesamten Zeit, in der ich mit Oracle gearbeitet habe, habe ich vorinstallierte Pakete mehrmals rückentwickelt. Der Code einiger von ihnen ist vor den Augen von Fremden verborgen (sie werden als verpackt bezeichnet). Wenn es Ihnen verboten ist, etwas anzusehen, nimmt die Versuchung zu, herauszufinden, wie es im Inneren angeordnet ist, nur zu. Aber oft gibt es auch nach dem Anvrapper nicht immer etwas zu sehen, da die Funktionen solcher Pakete als c-Schnittstelle zu so-Bibliotheken auf der Festplatte implementiert sind.
Insgesamt haben wir uns an einen Kandidaten für die Implementierung gewandt - Technologie mit externen Verfahren .
Die speziell gestaltete Bibliothek kann Methoden exportieren, die dann die Oracle-Datenbank über pl / sql aufrufen kann. Scheint vielversprechend. Nur einmal traf ich dies in Advanced plsql Kursen, so dass ich mich sehr aus der Ferne daran erinnerte, wie man es kocht. Und es bedeutet, dass die Dokumentation gelesen werden muss. Ich las es - und wurde sofort depressiv. Da das Laden einer solchen benutzerdefinierten Bibliothek in einem separaten Agentenprozess über einen Datenbank-Listener erfolgt und die Kommunikation mit diesem Agenten über dlink erfolgt. Unsere Idee war es also, eine Umgebungsvariable innerhalb des Datenbankprozesses selbst festzulegen. Und das alles aus Sicherheitsgründen.

Ein Bild aus der Dokumentation, das zeigt, wie es funktioniert:



Der Typ der so / dll-Bibliothek ist nicht so wichtig, aber aus irgendeinem Grund ist das Bild nur für Windows.

Vielleicht hat hier jemand eine weitere mögliche Gelegenheit bemerkt. Ja, ja, das ist Java. Mit Oracle können Sie gespeicherten Prozedurcode nicht nur in plsql, sondern auch in Java schreiben, die jedoch genauso wie plsql-Methoden exportiert werden. In regelmäßigen Abständen habe ich dies getan, daher sollte es kein Problem damit geben. Aber dann wurde eine weitere Falle versteckt. Java arbeitet mit einer Kopie der Umgebung und ermöglicht es Ihnen, nur die Umgebungsvariablen abzurufen, die der JVM-Prozess beim Start hatte. Die integrierte JVM erbt die Umgebungsvariablen des Datenbankprozesses, aber das ist alles. Ich habe im Internet Tipps gesehen, wie man die schreibgeschützte Karte durch Reflexion ändert, aber worum geht es, denn es ist immer noch nur eine Kopie. Das heißt, die Frau hatte wieder nichts mehr.

Java ist jedoch nicht nur wertvolles Fell. Mit ihm können Sie Prozesse aus einem Datenbankprozess heraus erzeugen. Obwohl alle unsicheren Vorgänge separat über den Java-Grant-Mechanismus gelöst werden müssen, der mit dem Paket dbms_java ausgeführt wird. Innerhalb des plsql-Codes können Sie die Prozess-PID des aktuellen Serverprozesses abrufen, in dem der Code ausgeführt wird, indem Sie die Systemansichten v $ session und v $ process verwenden. Außerdem können wir einen untergeordneten Prozess aus unserer Sitzung erzeugen, um etwas mit dieser PID zu tun. Zu Beginn habe ich einfach alle Umgebungsvariablen abgeleitet, die sich im Datenbankprozess befinden (um die Hypothese zu testen).

#!/bin/sh

pid=$1

awk 'BEGIN {RS="\0"; ORS="\n"} $0' "/proc/$pid/environ"

Gut abgeleitet, und was dann? Es ist immer noch unmöglich, die Variablen in der Umgebungsdatei zu ändern. Dies sind die Daten, die beim Start an den Prozess übertragen wurden und schreibgeschützt sind.

Ich habe im Stackoverflow im Internet gesucht: "So ändern Sie eine Umgebungsvariable in einem anderen Prozess." Die meisten Antworten waren, dass es unmöglich war, aber es gab eine Antwort, die diese Gelegenheit als minderwertigen und schmutzigen Hack beschrieb. Und diese Antwort war Albert Einstein gdb. Der Debugger kann sich an jeden Prozess anschließen, der seine PID kennt, und jede Funktion / Prozedur ausführen, die darin als öffentlich exportiertes Symbol vorhanden ist, beispielsweise aus einer Bibliothek. In libc gibt es Funktionen zum Arbeiten mit Umgebungsvariablen, und libc wird in jeden Prozess der Oracle-Datenbank (und praktisch in jedes Programm unter Linux) geladen.

So wird die Umgebungsvariable in einem fremden Prozess festgelegt (Sie müssen sie aufgrund des verwendeten ptrace von root aus aufrufen):

#!/bin/sh

pid=$1
env_name=$2
env_val="$3"

out=`gdb -q -batch -ex "attach $pid" -ex 'call (int) setenv("'$env_name'", "'"$env_val"'", 1)' -ex "detach" 2>&1`


Es ist auch geeignet, die Umgebungsvariablen innerhalb des GDB-Prozesses anzuzeigen. Wie bereits erwähnt, zeigt die Umgebungsdatei aus / proc / pid / nur die Variablen an, die zu Beginn des Prozesses vorhanden waren. Und wenn der Prozess im Laufe seiner Arbeit etwas geschaffen hat, kann dies nur durch den Debugger gesehen werden:
#!/bin/sh

pid=$1
var_name=$2

var_value=`gdb -q -batch -ex "attach $pid" -ex 'call (char*) getenv("'$var_name'")' -ex 'detach' | egrep '^\$1 ='`

if [ "$var_value" == '$1 = 0x0' ]
then
  # variable empty or does not exist
  echo -n
else
  # gdb returns $1 = hex_value "string value"
  var_hex=`echo "$var_value" | awk '{print $3}'`
  var_value=`echo "$var_value" | sed -r -e 's/^\$1 = '$var_hex' //;s/^"//;s/"$//'`
  
  echo -n "$var_value"
fi


Die Lösung ist also bereits in unserer Tasche - über Java erzeugen wir den Debugger-Prozess, der zu dem Prozess geht, der ihn generiert hat, die gewünschte Umgebungsvariable dafür festlegt und dann endet (der Moor hat seine Arbeit erledigt - der Moor kann gehen). Aber es gab das Gefühl, dass es eine Art Krücke war. Ich wollte etwas eleganteres. Es wäre irgendwie egal, den Datenbankprozess selbst zu zwingen, Umgebungsvariablen ohne externen Angriff festzulegen.

Ein Ei in einer Ente, eine Ente in einem Hasen ...


Und dann kommt jemand zur Rettung, ja, Sie haben es richtig erraten, wieder Java, nämlich JNI (Java Native Interface). Mit JNI können Sie native C-Methoden innerhalb der JVM aufrufen. Der Code wird auf besondere Weise in Form eines gemeinsam genutzten Objekts der Bibliothek ausgegeben, das die JVM dann lädt, während die Methoden in der Bibliothek den Java-Methoden in der Klasse zugeordnet sind, die mit dem nativen Modifikator deklariert wurden.

Nun, ok, wir schreiben eine Klasse (tatsächlich ist dies nur ein Werkstück):

public class Posix {

    private static native int setenv(String key, String value, boolean overwrite);

    private static native String getenv(String key);
    
    public static void stub() 
    {
        
    }
}

Kompilieren Sie es anschließend und rufen Sie die generierte h-Datei der zukünftigen Bibliothek ab:

#  
javac Posix.java

#   Posix.h        JNI
javah Posix

Nachdem wir die Header-Datei erhalten haben, schreiben wir den Body für jede Methode:

#include <stdlib.h>
#include "Posix.h"

JNIEXPORT jint JNICALL Java_Posix_setenv(JNIEnv *env, jclass cls, jstring key, jstring value, jboolean overwrite)
{
    char* k = (char *) (*env)->GetStringUTFChars(env, key, NULL);
    char* v = (char *) (*env)->GetStringUTFChars(env, value, NULL);

    int err = setenv(k, v, overwrite);

    (*env)->ReleaseStringUTFChars(env, key, k);
    (*env)->ReleaseStringUTFChars(env, value, v);

    return err;
}

JNIEXPORT jstring JNICALL Java_Posix_getenv(JNIEnv *env, jclass cls, jstring key)
{
    char* k = (char *) (*env)->GetStringUTFChars(env, key, NULL);
    char* v = getenv(k);

    return (*env)->NewStringUTF(env, v);
}

und kompilieren Sie die Bibliothek

gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -fPIC Posix.c -shared -o libPosix.so -Wl,-soname -Wl,--no-whole-archive

strip libPosix.so

Damit Java die native Bibliothek laden kann, muss sie vom System ld gemäß allen Linux-Regeln gefunden werden. Darüber hinaus verfügt Java über eine Reihe von Eigenschaften, die die Pfade enthalten, in denen die Bibliothekssuche stattfindet. Der einfachste Weg, um in Oracle zu arbeiten, besteht darin, unsere Bibliothek in $ ORACLE_HOME / lib abzulegen.

Nachdem wir die Bibliothek erstellt haben, müssen wir die Klasse in der Datenbank kompilieren und als plsql-Paket veröffentlichen. Es gibt zwei Optionen zum Erstellen von Java-Klassen in der Datenbank:

  • Laden Sie die binäre Klassendatei über das Dienstprogramm loadjava
  • Kompilieren Sie Klassencode aus der Quelle mit sqlplus

Wir werden die zweite Methode verwenden, obwohl sie im Grunde gleich sind. Für den ersten Fall war es notwendig, sofort den gesamten Klassencode in Stufe 1 zu schreiben, als wir eine Stub-Klasse für die h-Datei erhielten.

Um eine Klasse in subd zu erstellen, wird eine spezielle Syntax verwendet:

CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED "Posix" AS
...
...
/

Wenn die Klasse erstellt wird, muss sie als plsql-Methode veröffentlicht werden, und auch hier die spezielle Syntax:

procedure set_env(var_name varchar2, var_value varchar2)
is
language java name 'Posix.set_env(java.lang.String, java.lang.String)';

Wenn Sie versuchen, potenziell unsichere Methoden in Java aufzurufen, wird eine Ausführung ausgelöst, die besagt, dass für den Benutzer keine Java-Berechtigung erteilt wurde. Das Laden nativer Methoden ist eine weitere unsichere Operation, da wir fremden Code direkt in den Datenbankprozess einfügen (der gleiche Exploit, der im Header angekündigt wurde).

Da es sich bei der Datenbank jedoch um einen Test handelt, gewähren wir einen Zuschuss ohne Bedenken hinsichtlich der Verbindung von sys:

begin
dbms_java.grant_permission( 'SYSTEM', 'SYS:java.lang.RuntimePermission', 'loadLibrary.Posix', '');
commit;
end;
/

Der Systembenutzername ist derjenige, in dem ich den Java-Code und das plsql-Wrapper-Paket kompiliert habe.
Es ist wichtig zu beachten, dass beim Laden einer Bibliothek über einen Aufruf von System.loadLibrary das lib-Präfix und die so-Erweiterung (wie in der Dokumentation beschrieben) weggelassen werden und kein Pfad übergeben wird, in dem gesucht werden soll. Es gibt eine ähnliche System.load-Methode, mit der eine Bibliothek nur über einen absoluten Pfad geladen werden kann.

Und dann erwarten uns 2 unangenehme Überraschungen - ich landete im nächsten Kaninchenbau von Oracle. Bei der Erteilung eines Zuschusses tritt ein Fehler mit einer ziemlich nebligen Meldung auf:

ORA-29532: Java call terminated by uncaught Java exception: java.lang.SecurityException: policy table update

Das Problem wird im Internet gegoogelt und führt zu My Oracle Support (auch bekannt als Metalink). weil Gemäß den Oracle-Regeln ist das Veröffentlichen von Artikeln von einem Metalink in Open Source nicht gestattet. Ich erwähne nur die Dokumentnummer 259471.1 (diejenigen, die Zugriff haben, können selbst lesen).

Das Wesentliche des Problems ist, dass Oracle nicht zulässt, dass verdächtiger Code von Drittanbietern in unseren Prozess geladen wird. Welches ist logisch.

Da es sich bei der Basis jedoch um einen Test handelt und wir uns auf unseren Code verlassen können, erlauben wir den Download ohne besondere Befürchtungen.
Fuh, Missgeschicke sind vorbei.

Es lebt, lebt


Mit angehaltenem Atem beschloss ich, meinem Frankenstein Leben einzuhauchen.
Wir starten die Datenbank mit der vorinstallierten libfaketime und dem 0-Offset. Stellen Sie
eine Verbindung zur Datenbank her und rufen Sie den Code auf, der einfach die Zeit vor und nach dem Ändern der Umgebungsvariablen anzeigt:

begin
dbms_output.enable(100000);
dbms_java.set_output(100000);
dbms_output.put_line('old time: '||to_char(sysdate, 'dd.mm.yyyy hh24:mi:ss'));
system.posix.set_env('FAKETIME','+1d');
dbms_output.put_line('new time: '||to_char(sysdate, 'dd.mm.yyyy hh24:mi:ss'));
end;
/


Es funktioniert, verdammt! Ehrlich gesagt hatte ich weitere Überraschungen erwartet, wie zum Beispiel ORA-600-Fehler. Die Warnung hatte jedoch die ganze Nummer und der Code funktionierte weiter.
Es ist wichtig zu beachten, dass, wenn die Verbindung zur Datenbank als dediziert hergestellt wird, der Prozess nach Abschluss der Verbindung zerstört wird und keine Ablaufverfolgung erfolgt. Wenn wir jedoch gemeinsam genutzte Verbindungen verwenden, wird in diesem Fall ein vorgefertigter Prozess aus dem Serverpool zugewiesen. Wir ändern die Zeit darin durch Umgebungsvariablen. Wenn die Verbindung getrennt wird, bleibt sie innerhalb des Prozesses geändert. Und wenn dann eine andere Datenbanksitzung in denselben Serverprozess fällt, wird sie zu ihrer erheblichen Überraschung die falsche Zeit erhalten. Daher ist es am Ende der Testsitzung besser, die Zeit immer auf Null zurückzusetzen.

Fazit


Ich hoffe, die Geschichte war interessant (und vielleicht sogar für jemanden nützlich).

Quellcodes sind alle auf Github verfügbar .

Die libfaketime Dokumentation zu .

Wie testest du? Und wie erstellen Sie Entwicklungs- und Testdatenbanken in einem Unternehmen?

Bonus für diejenigen, die bis zum Ende lesen


All Articles