Cuento de cómo hacer una máquina del tiempo para una base de datos y escribir accidentalmente un exploit

Buen dia, Habr.

¿Alguna vez te has preguntado cómo cambiar el tiempo dentro de la base de datos? ¿Fácil? Bueno, en algunos casos, sí, es fácil: el comando de Linux es date y el punto está en el sombrero. ¿Y si necesita cambiar la hora solo dentro de una instancia de la base de datos si hay varias en el servidor? ¿Y para un solo proceso de base de datos? ¿Y? Uh, eso es, mi amigo, ese es el punto.Alguien dirá que este es otro sur, no relacionado con la realidad, que se presenta periódicamente en Habré. Pero no, la tarea es bastante real y está dictada por la necesidad de producción: pruebas de código. Aunque estoy de acuerdo, el caso de prueba puede ser bastante exótico: compruebe cómo se comporta el código para una fecha determinada en el futuro. En este artículo, examinaré en detalle cómo se resolvió esta tarea, y al mismo tiempo capturaré un poco el proceso de organización de pruebas y dev representa la base de Oracle. Antes de una lectura larga, póngase cómodo y pida un gato.

Antecedentes


Comencemos con una breve introducción para mostrar por qué esto es necesario. Como ya se anunció, escribimos pruebas al implementar ediciones en la base de datos. El sistema bajo el cual se realizan estas pruebas se desarrolló al principio (o tal vez un poco antes del inicio) de los cero, por lo que toda la lógica de negocios está dentro de la base de datos y está escrita en forma de procedimientos almacenados en el lenguaje pl / sql. Y sí, nos trae dolor y sufrimiento. Pero este es un legado, y tienes que vivir con él. En el código y el modelo tabular, es posible especificar cómo evolucionan los parámetros dentro del sistema con el tiempo, en otras palabras, establecer la actividad desde qué fecha y a qué fecha se pueden aplicar. Qué llegar lejos: el cambio reciente en la tasa del IVA es un claro ejemplo de esto. Y para que tales cambios en el sistema puedan verificarse por adelantado,una base de datos con tales cambios debe transferirse a una fecha determinada en el futuro, los parámetros de código en las tablas se activarán en el "momento actual". Y debido a las características específicas del sistema compatible, no puede utilizar pruebas simuladas que simplemente cambiarían el valor de retorno de la fecha actual del sistema en el idioma en que comienza la sesión de prueba.

Entonces, determinamos por qué, luego necesitamos determinar cómo se logra el objetivo. Para hacer esto, haré una pequeña retrospectiva de las opciones para construir bancos de prueba para desarrolladores y cómo comenzó cada sesión de prueba.

Edad de Piedra


Érase una vez, cuando los árboles eran pequeños, y los mainframes eran grandes, solo había un servidor para el desarrollo y también estaba realizando pruebas. Y, en principio, todo esto fue suficiente para todos (¡ 640K es suficiente para todos! )

Contras: para la tarea de cambiar el tiempo, era necesario involucrar a muchos departamentos relacionados: administradores del sistema (haciendo el cambio de hora en el servidor secundario desde la raíz), administradores de DBMS (haciendo el reinicio de la base de datos), programadores ( fue necesario notificar que se produciría un cambio de hora, porque parte del código dejó de funcionar, por ejemplo, los tokens web emitidos anteriormente para llamar a métodos api dejaron de ser válidos y esto podría ser una sorpresa), probadores (probándose a sí mismo) ... Cuando devuelve el tiempo al presente todo se repitió en orden inverso.

Edades medias


Con el tiempo, el número de desarrolladores en el departamento creció y en algún momento 1 servidor dejó de ser suficiente. Principalmente debido al hecho de que diferentes desarrolladores quieren cambiar el mismo paquete pl / sql y realizar pruebas para él (incluso sin cambiar el tiempo). Se escuchó más y más indignación: “¡Cuánto tiempo! ¡Basta de tolerar esto! ¡Fábricas para trabajadores, tierras para campesinos! ¡Cada programador tiene una base de datos! Sin embargo, si tiene unos pocos terabytes de base de datos de productos y 50-100 desarrolladores, honestamente en este formulario, el requisito no es muy real. Y aún así, todos quieren que la base de prueba y desarrollo no se quede muy atrás de las ventas, tanto en estructura como en los datos dentro de las tablas. Entonces había un servidor separado para las pruebas, llamémoslo preproducción. Fue construido a partir de 2 servidores idénticos,donde se realizó la venta para restaurar la base de datos de dólares RMAN y tardó entre 2 y 2,5 días. Después de la recuperación, la base de datos realizó el anonimato de datos personales y otros datos importantes y la carga de las aplicaciones de prueba se aplicó a este servidor (así como los programadores siempre trabajaron con el servidor recientemente restaurado). El trabajo con el servidor requerido se aseguró utilizando el recurso de IP del clúster compatible con corosync (marcapasos). Mientras todos trabajan con el servidor activo, en el segundo nodo, la recuperación de la base de datos comienza nuevamente y después de 2-3 días cambian nuevamente de lugar.El trabajo con el servidor requerido se aseguró utilizando el recurso de IP del clúster compatible con corosync (marcapasos). Mientras todos trabajan con el servidor activo, en el segundo nodo, la recuperación de la base de datos comienza nuevamente y después de 2-3 días cambian nuevamente de lugar.El trabajo con el servidor requerido se aseguró utilizando el recurso de IP del clúster compatible con corosync (marcapasos). Mientras todos trabajan con el servidor activo, en el segundo nodo, la recuperación de la base de datos comienza nuevamente y después de 2-3 días cambian nuevamente de lugar.

De las desventajas obvias: necesita 2 servidores y 2 veces más recursos (principalmente discos) que productos.

Pros: operación y prueba de cambio de hora: se puede realizar en el segundo servidor, en el servidor principal en este momento, los desarrolladores viven y se dedican a sus negocios. El cambio de servidor ocurre solo cuando la base de datos está lista y el tiempo de inactividad del entorno de prueba es mínimo.

La era del progreso científico y tecnológico.


Cuando cambiamos a la base de datos 11g Release 2, leemos acerca de una tecnología interesante que Oracle proporciona bajo el nombre de CloneDB. La conclusión es que las copias de seguridad de la base de datos del producto (hay una copia directa de los archivos de datos del producto) se almacenan en un servidor especial, que luego publica este conjunto de archivos de datos a través de DNFS (NFS directo) a prácticamente cualquier número de servidores, y no es necesario tener uno en el servidor la misma cantidad de discos, porque se implementa el enfoque Copiar en escritura: la base de datos utiliza un recurso compartido de red con archivos de datos del servidor de respaldo para leer datos en tablas, y los cambios se escriben en archivos de datos locales en el servidor de desarrollo. Periódicamente, se realiza la "puesta a cero de los plazos" para el servidor, de modo que los archivos de datos locales no crecen demasiado y el lugar no termina. Al actualizar el servidor, los datos también se despersonalizan en las tablas,en este caso, todas las actualizaciones de tablas caen en archivos de datos locales y esas tablas se leen desde el servidor local, todas las demás tablas se leen a través de la red.

Contras: todavía hay 2 servidores (para garantizar actualizaciones fluidas con un tiempo de inactividad mínimo para los consumidores), pero ahora el volumen de discos se reduce considerablemente. Para almacenar dólares en una bola nfs, necesita 1 servidor más en tamaño + - como producto, pero el tiempo de ejecución de la actualización se reduce (especialmente cuando se usan dólares incrementales). La conexión en red con una bola nfs ralentiza notablemente las operaciones de lectura de E / S. Para usar la tecnología CloneDB, la base debe ser una Edición Enterprise; en nuestro caso, tuvimos que llevar a cabo el procedimiento de actualización en las bases de prueba cada vez. Afortunadamente, las bases de datos de prueba están exentas de las políticas de licencias de Oracle.

Pros: la operación para restaurar una base de un bakup lleva menos de 1 día (no recuerdo la hora exacta).

Cambio de tiempo: sin cambios importantes. Aunque en este momento ya se habían hecho scripts para cambiar la hora en el servidor y reiniciar la base de datos para hacer esto sin llamar la atención de los ordenados de los administradores.

Era de nueva historia


Para ahorrar aún más espacio en disco y hacer que la lectura de datos fuera de línea, decidimos implementar nuestra versión CloneDB (con flashback e instantáneas) usando un sistema de archivos con compresión. Durante las pruebas preliminares, la elección recayó en ZFS, aunque no hay soporte oficial para ello en el kernel de Linux (cita del artículo) A modo de comparación, también analizamos BTRFS (b-tree fs), que Oracle está promoviendo, pero la relación de compresión fue menor con el mismo consumo de CPU y RAM en las pruebas. Para habilitar el soporte de ZFS en RHEL5, se construyó su propio núcleo basado en UEK (núcleo empresarial irrompible), y en los ejes y núcleos más nuevos, simplemente puede usar el núcleo UEK listo para usar. La implementación de una base de prueba de este tipo también se basa en el mecanismo COW, pero a nivel de las instantáneas del sistema de archivos. Se suministran 2 dispositivos de disco al servidor, en uno, se crea el grupo zfs, donde a través de RMAN se crea una base de datos de reserva adicional de la venta, y dado que usamos compresión, la partición ocupa menos que la producción.
El sistema se instala en el segundo dispositivo de disco y el resto es necesario para que el servidor y la base de datos funcionen, por ejemplo, particiones para deshacer y temp. En cualquier momento, puede hacer una instantánea del grupo zfs, que luego se abre como una base de datos separada. Crear una instantánea toma un par de segundos. ¡Es magia! Y, en principio, tales bases de datos pueden inclinarse bastante, si solo el servidor tuviera suficiente RAM para todas las instancias y el tamaño del grupo zfs en sí (para almacenar cambios en los archivos de datos durante la despersonalización y durante el ciclo de vida del clon de la base de datos). El momento principal para actualizar la base de prueba es la operación de despersonalización de datos, pero también cabe en 15-20 minutos. Hay una aceleración significativa.

Contras: en el servidor no puede cambiar la hora simplemente traduciendo la hora del sistema, porque todas las instancias de la base de datos que se ejecutan en este servidor caerán en este momento de una vez. Se ha encontrado una solución a este problema y se describirá en la sección correspondiente. Mirando hacia el futuro, diré que le permite cambiar la hora dentro de solo 1 instancia de la base de datos (enfoque de cambio de hora por instancia) sin afectar al resto en el mismo servidor. Y el tiempo en el servidor en sí tampoco cambia. Esto elimina la necesidad de un script raíz para cambiar la hora en el servidor. También en esta etapa, se implementa la automatización de cambio de tiempo para instancias a través de Jenkins CI y los usuarios (en términos relativos, equipos de desarrollo) que poseen su stand tienen derecho a los trabajos a través de los cuales ellos mismos pueden cambiar el horario, actualizar el stand al estado actual con ventas, tomar instantáneas y restauración (reversión) de la base a la instantánea creada previamente.

Era de la historia reciente


Con la llegada de Oracle 12c, apareció una nueva tecnología: bases de datos conectables y, como resultado, bases de datos de contenedores (cdb). Con esta tecnología, dentro de una instancia física, se pueden crear varias bases de datos "virtuales" que comparten un área de memoria común de la instancia. Pros: puede guardar memoria para el servidor (y aumentar el rendimiento general de nuestra base de datos, porque toda la memoria que estaba ocupada antes, por ejemplo, 5 instancias diferentes, se puede compartir para todos los contenedores pdb implementados dentro de cdb, y solo la usarán cuando realmente lo necesitan, y no como lo fue en la fase anterior, cuando cada instancia "bloqueó" la memoria que se le asignó y cuando la actividad de uno de los clones fue baja, la memoria no se usó de manera efectiva, en otras palabras, estaba inactiva).Los archivos de datos de diferentes pdb todavía se encuentran en el grupo zfs, y cuando implementan clones, usan el mismo mech de instantáneas zfs. En esta etapa, nos acercamos lo suficiente a la capacidad de dar a casi todos los desarrolladores su propia base de datos. Cambiar el tiempo en esta etapa no requiere un reinicio de la base de datos y funciona con mucha precisión solo para aquellos procesos que necesitan un cambio de tiempo; todos los demás usuarios que trabajan con esta base de datos no se ven afectados de ninguna manera.

Menos: no puede utilizar el enfoque de cambio de tiempo por instancia de la fase anterior, porque ahora tenemos una instancia. Sin embargo, se encontró una solución para este caso. Y fue precisamente esto lo que sirvió de impulso para escribir este artículo. Mirando hacia el futuro, diré que es un cambio de tiempo por enfoque de proceso , es decir en cada proceso de base de datos, puede establecer su propio tiempo único en general.

En este caso, una sesión de prueba típica inmediatamente después de conectarse a la base de datos establece el momento adecuado al comienzo de su trabajo, realiza pruebas y devuelve el tiempo al final. Es necesario devolver el tiempo por una simple razón: algunos procesos de la base de datos Oracle no terminan cuando el cliente de la base de datos se desconecta del servidor, estos son procesos del servidor llamados servidores compartidos, que, a diferencia de los procesos dedicados, se ejecutan cuando el servidor de la base de datos se inicia y vive casi indefinidamente (en el ideal imagen del mundo). Si deja la hora cambiada en dicho proceso del servidor, entonces otra conexión que se servirá en este proceso recibirá la hora incorrecta.

En nuestro sistema, los servidores compartidos se usan mucho, porque hasta 11 g prácticamente no había una solución adecuada para que nuestro sistema soportara cargas elevadas (en 11 g apareció DRCP - agrupación de conexiones residentes en la base de datos). Y he aquí por qué: en sub hay un límite en el número total de procesos del servidor que puede crear tanto en modo dedicado como compartido. Los procesos dedicados se generan más lentamente de lo que la base de datos puede emitir un proceso compartido ya preparado del conjunto de procesos compartidos, lo que significa que cuando las nuevas conexiones están llegando constantemente (especialmente si el proceso realiza otras operaciones lentas), el número total de procesos aumentará. Cuando se alcanza el límite de sesiones / procesos, la base de datos deja de dar servicio a nuevas conexiones y se produce el colapso.La transición al uso de un grupo de procesos compartidos nos permitió reducir la cantidad de procesos nuevos en el servidor al conectarnos.

Ahí es donde se completa la revisión de las tecnologías para construir bases de datos de prueba y finalmente podemos comenzar a implementar los algoritmos de cambio de tiempo para la base de datos en sí.

El enfoque falso por instancia


¿Cómo cambiar el tiempo dentro de la base de datos?

Lo primero que se me ocurrió fue crear en un esquema que contiene todo el código de lógica de negocios su propia función que se superpone a las funciones del lenguaje que funcionan con el tiempo (sysdate, current_date, etc.) y, bajo ciertas condiciones, comienza a dar otros valores, por ejemplo, podría establecer valores a través del contexto de la sesión al comienzo de la ejecución de la prueba. No funcionó, las funciones de lenguaje incorporadas no se superponen con las del usuario.

Luego, se probaron los sistemas de virtualización ligera (Vserver, OpenVZ) y la contenedorización a través de Docker. Tampoco funciona, usan el mismo núcleo que el sistema host, lo que significa que usan los mismos valores de temporizador del sistema. Caerse de nuevo.

Y aquí no tengo miedo de rescatar esta palabra, una invención brillante del mundo Linux: redefinición / intercepción de funciones en la etapa de carga dinámica de objetos compartidos. Muchos lo conocen como trucos con LD_PRELOAD. En la variable de entorno LD_PRELOAD, puede especificar la biblioteca que se cargará antes que todas las demás que el proceso necesita, y si esta biblioteca tiene caracteres con el mismo nombre que, por ejemplo, en la biblioteca estándar, que se cargará más tarde, la tabla de importación de símbolos para la aplicación se verá como si la función proporciona nuestro módulo de reemplazo. Y eso es exactamente lo que hace la biblioteca del proyecto libfaketimeque comenzamos a usar para iniciar la base de datos en un momento diferente al del sistema. La biblioteca pierde llamadas relacionadas con trabajar con el temporizador del sistema y obtener la hora y fecha del sistema. Para controlar cuánto tiempo se mueve en relación con la fecha actual del servidor o desde qué momento debe pasar el tiempo dentro del proceso, todo está controlado por variables de entorno que deben establecerse junto con LD_PRELOAD. Para implementar el cambio de hora, implementamos un trabajo en el servidor Jenkins, que ingresa al servidor de la base de datos y reinicia el DBMS con o sin variables de entorno establecidas para libfaketime.

Un algoritmo de ejemplo para iniciar una base de datos con un tiempo de sustitución:

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

Y si piensas que todo funcionó de inmediato, entonces estás profundamente equivocado. Porque, como resultó, valida las bibliotecas que se cargan en el proceso cuando se inicia el DBMS. Y en el registro de alertas, comienza a resentirse por la falsificación notada, mientras que la base no comienza. Ahora no recuerdo exactamente cómo deshacerme de él, hay algún parámetro que puede deshabilitar la ejecución de controles de cordura al inicio.

El enfoque falso por proceso


La idea general de cambiar el tiempo solo dentro de 1 proceso siguió siendo la misma: use libfaketime. Comenzamos la base de datos con una biblioteca precargada, pero establecemos un desplazamiento de tiempo cero al inicio, que luego se propaga a todos los procesos DBMS. Y luego, dentro de la sesión de prueba, configure la variable de entorno solo para este proceso. Pff, algo de negocios.

Sin embargo, para aquellos que están familiarizados con el lenguaje pl / sql, todo el destino de esta idea es inmediatamente claro. Porque el lenguaje es muy limitado y básicamente adecuado para tareas de alto nivel. No se puede implementar la programación del sistema allí. Aunque algunas operaciones de bajo nivel (por ejemplo, trabajar con una red, trabajar con archivos) están presentes en forma de paquetes dbms / utl preinstalados del sistema. Durante todo el tiempo que trabajé con Oracle, realicé ingeniería inversa de paquetes preinstalados varias veces, el código de algunos de ellos está oculto a los ojos de extraños (se llaman envueltos). Si tiene prohibido mirar algo, entonces la tentación de descubrir cómo está organizada en el interior solo aumenta. Pero a menudo, incluso después del anvrapper, no siempre hay algo que ver, porque las funciones de dichos paquetes se implementan como interfaz c para las bibliotecas en el disco.
En total, nos acercamos a un candidato para la implementación: tecnología con procedimientos externos .
La biblioteca diseñada de manera especial puede exportar métodos, que luego la base de datos Oracle puede llamar a través de pl / sql. Parece prometedor Solo una vez que conocí esto en los cursos avanzados de plsql, recordé muy remotamente cómo cocinarlo. Y significa que es necesario leer la documentación. Lo leí e inmediatamente me deprimí. Debido a que la carga de dicha biblioteca personalizada se realiza en un proceso de agente separado a través de un escucha de base de datos, y la comunicación con este agente se realiza a través de dlink. Entonces, nuestra idea fue establecer una variable de entorno dentro del proceso de la base de datos. Y todo esto se hace por razones de seguridad.

Una imagen de la documentación que muestra cómo funciona:



El tipo de la biblioteca so / dll no es tan importante, pero por alguna razón la imagen es solo para Windows.

Quizás alguien notó aquí otra oportunidad potencial. Sí, sí, esto es Java. Oracle le permite escribir código de procedimiento almacenado no solo en plsql, sino también en java, que sin embargo se exportan de la misma manera que los métodos plsql. Periódicamente, hice esto, por lo que no debería haber un problema con esto. Pero luego se escondió otra trampa. Java funciona con una copia del entorno y le permite obtener solo las variables de entorno que el proceso JVM tenía al inicio. La JVM incorporada hereda las variables de entorno del proceso de la base de datos, pero eso es todo. Vi consejos en Internet sobre cómo cambiar el mapa de solo lectura a través de la reflexión, pero cuál es el punto, porque todavía es solo una copia. Es decir, la mujer quedó nuevamente sin nada.

Sin embargo, Java no es solo un pelaje valioso. Al usarlo, puede generar procesos desde un proceso de base de datos. Aunque todas las operaciones inseguras deben resolverse por separado a través del mecanismo de concesiones de Java, que se realizan utilizando el paquete dbms_java. Desde el interior del código plsql, puede obtener el proceso pid del proceso del servidor actual en el que se está ejecutando el código, utilizando las vistas del sistema v $ session y v $ process. Además, podemos generar algunos procesos secundarios de nuestra sesión para hacer algo con este pid. Para comenzar, simplemente deduje todas las variables de entorno que están dentro del proceso de la base de datos (para probar la hipótesis)

#!/bin/sh

pid=$1

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

Bien deducido, y luego qué. Todavía es imposible cambiar las variables en el archivo de entorno, estos son los datos que se transfirieron al proceso cuando comenzó y son de solo lectura.

Busqué en Internet en stackoverflow "Cómo cambiar una variable de entorno en otro proceso". La mayoría de las respuestas fueron que era imposible, pero hubo una respuesta que describió esta oportunidad como un truco sucio y deficiente. Y esa respuesta fue Albert Einstein gdb. El depurador puede conectarse a cualquier proceso conociendo su pid y ejecutar cualquier función / procedimiento que exista en él como símbolo exportado públicamente, por ejemplo, desde alguna biblioteca. En libc, hay funciones para trabajar con variables de entorno, y libc se carga en cualquier proceso de la base de datos Oracle (y prácticamente en cualquier programa en Linux).

Así es como se establece la variable de entorno en un proceso extraño (debe llamarlo desde la raíz debido al ptrace utilizado):

#!/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`


Además, para ver las variables de entorno dentro del proceso gdb también es adecuado. Como se mencionó anteriormente, el archivo entorno de / proc / pid / muestra solo las variables que existían al comienzo del proceso. Y si el proceso creó algo en el curso de su trabajo, esto solo se puede ver a través del depurador:
#!/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


Entonces, la solución ya está en nuestro bolsillo: a través de Java generamos el proceso de depuración, que va al proceso que lo generó y establece la variable de entorno deseada para él y luego termina (el moro ha hecho su trabajo, el moro puede irse). Pero tenía la sensación de que era una especie de muleta. Quería algo más elegante. De alguna manera, sería lo mismo forzar al proceso de la base de datos a establecer variables de entorno sin asalto externo.

Un huevo en un pato, un pato en una liebre ...


Y luego alguien viene al rescate, sí, lo has adivinado bien, nuevamente Java, es decir, JNI (interfaz nativa de Java). JNI le permite llamar a métodos C nativos dentro de la JVM. El código se emite de manera especial en forma de un objeto compartido de la biblioteca, que luego carga la JVM, mientras que los métodos que estaban en la biblioteca se asignan a los métodos de Java dentro de la clase declarada con el modificador nativo.

Bueno, ok, estamos escribiendo una clase (de hecho, esto es solo una pieza de trabajo):

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() 
    {
        
    }
}

Después de eso, compílelo y obtenga el archivo h generado de la futura biblioteca:

#  
javac Posix.java

#   Posix.h        JNI
javah Posix

Habiendo recibido el archivo de encabezado, escribimos el cuerpo para cada método:

#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);
}

y compilar la biblioteca

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

Para que Java cargue la biblioteca nativa, debe ser encontrada por el sistema ld de acuerdo con todas las reglas de Linux. Además, Java tiene un conjunto de propiedades que contienen las rutas donde se realizan las búsquedas en la biblioteca. La forma más fácil de trabajar dentro de Oracle es poner nuestra biblioteca en $ ORACLE_HOME / lib.

Y después de haber creado la biblioteca, necesitamos compilar la clase dentro de la base de datos y publicarla como un paquete plsql. Hay 2 opciones para crear clases Java dentro de la base de datos:

  • cargar el archivo de clase binario a través de la utilidad loadjava
  • compilar código de clase desde la fuente usando sqlplus

Usaremos el segundo método, aunque son básicamente iguales. Para el primer caso, fue necesario escribir inmediatamente todo el código de clase en la etapa 1, cuando recibimos una clase de código auxiliar para el archivo h.

Para crear una clase en subd, se utiliza una sintaxis especial:

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

Cuando se crea la clase, debe publicarse como métodos plsql, y aquí nuevamente la sintaxis especial:

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

Cuando intenta llamar a métodos potencialmente inseguros dentro de Java, se genera una ejecución que dice que no se ha emitido ninguna concesión de Java para el usuario. La carga de métodos nativos es otra operación insegura, porque inyectamos código extraño directamente en el proceso de la base de datos (el mismo exploit que se anunció en el encabezado).

Pero dado que la base de datos es de prueba, otorgamos una subvención sin ninguna preocupación al conectarse desde sys:

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

El nombre de usuario del sistema es el que compilé el código de Java y el paquete de envoltorio plsql.
Es importante tener en cuenta que al cargar una biblioteca a través de una llamada a System.loadLibrary, omitimos el prefijo lib y la extensión so (como se describe en la documentación) y no pasamos ninguna ruta donde buscar. Existe un método similar de System.load que solo puede cargar una biblioteca utilizando una ruta absoluta.

Y luego nos esperan 2 desagradables sorpresas: aterricé en la próxima madriguera de conejos de Oracle. Al emitir una subvención, se produce un error con un mensaje bastante brumoso:

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

El problema se busca en Internet y conduce a My Oracle Support (también conocido como Metalink). Porque De acuerdo con las reglas de Oracle, no está permitido publicar artículos de un enlace de metal en fuentes abiertas, solo mencionaré el número de documento: 259471.1 (los que tengan acceso podrán leer por sí mismos).

La esencia del problema es que Oracle no nos permitirá simplemente cargar el código sospechoso de terceros en nuestro proceso. Lo cual es lógico.

Pero como la base es prueba y confiamos en nuestro código, permitimos la descarga sin miedos especiales.
Fuh, las desventuras han terminado.

Esta vivo, vivo


Con la respiración contenida, decidí intentar darle vida a mi Frankenstein.
Comenzamos la base de datos con el libfaketime precargado y el desplazamiento 0.
Conéctese a la base de datos y realice una llamada al código que simplemente muestra el tiempo antes y después de cambiar la variable de entorno:

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;
/


¡Funciona, maldita sea! Honestamente, esperaba algunas sorpresas más, como los errores de ORA-600. Sin embargo, la alerta tenía el número entero y el código seguía funcionando.
Es importante tener en cuenta que si la conexión a la base de datos se realiza como dedicada, luego de que se complete la conexión, el proceso se destruirá y no habrá rastros. Pero si usamos conexiones compartidas, en este caso se asigna un proceso listo desde el grupo de servidores, cambiamos el tiempo en él a través de variables de entorno y cuando se desconecta, permanecerá modificado dentro del proceso. Y cuando otra sesión de la base de datos caiga en el mismo proceso del servidor, recibirá la hora equivocada para su considerable sorpresa. Por lo tanto, al final de la sesión de prueba, es mejor volver siempre el tiempo a cero.

Conclusión


Espero que la historia haya sido interesante (y tal vez incluso útil para alguien).

Los códigos fuente están disponibles en Github .

La documentación de libfaketime también .

¿Cómo haces las pruebas? ¿Y cómo se crean bases de datos de desarrollo y prueba en una empresa?

Bono para aquellos que leen hasta el final


All Articles