DEV Community

Cover image for C#: Hackeo nocturno
Baltasar García Perez-Schofield
Baltasar García Perez-Schofield

Posted on • Updated on

C#: Hackeo nocturno

Ignacio está molesto. Resulta que como es la adquisición más reciente de la empresa, le ha tocado estar de guardia esta noche. Su empresa es el banco Sientebien, y él es parte del departamento de desarrollo.

Espera una noche tranquila y se echa una cabezadita con el respaldo de la silla reclinado. No es perfecto, pero es algo. Está en lo mejor del mundo de los sueños cuando le despierta la alarma del cliente de correo de la empresa cuando llega un correo urgente, de alta prioridad.

22/11/2023 01:05    arcangel@infra.sientebien.com    AVISO - Múltiples ingresos detectados
Enter fullscreen mode Exit fullscreen mode

— ¿Cómo? ¿Múltiples ingresos?

Ignacio maldice por lo bajo. Ese aviso solo se activa cuando se detectan ingresos por encima de 3000 €. ¡Y se están produciendo varios casi al mismo tiempo! A estas horas de la madrugada, es altamente sospechoso. Se dispone a comprobar el correo de error cuando recibe otro correo.

22/11/2023 01:05    hacker@jajaja.com    Hola mindundi
Enter fullscreen mode Exit fullscreen mode

— ¿hacker? ¿jajaja.com? ¿¡Mindundi!? No puede resistirse y abre primero el último correo.

Al mindundi que le ha tocado pringar esta noche:

Soy un gran hacker y vuestros programadores dan pena. Fíjate en lo que me habéis permitido hacer:

    using SienteBien.ExternalApi as SienteBienApi


    var usr = SienteBienApi.Users.Get( ..... );
    var tarjeta = usr.Tarjetas[0];

    tarjeta.Movimientos = new List<SienteBienApi.Movimiento> {
        new Movimiento{ Cantidad = -4000, Concepto = "ja!", Entidad = "jajaja.com" },
        new Movimiento{ Cantidad = -6000, Concepto = "ja!", Entidad = "jajaja.com" },
        new Movimiento{ Cantidad = -8000, Concepto = "ja!", Entidad = "jajaja.com" }
    };

    SienteBienApi.Commit();
Enter fullscreen mode Exit fullscreen mode

¡Nunca me pillarás! ¡Gracias mindundi!
Baltasator

Ignacio pega un respingo y salta de su silla. Lo primero que hace es ir corriendo a la sala de comunicaciones, abre el armario de red y desconecta EL CABLE. El que tiene una etiqueta roja pegada. El que permite acceso al exterior de la empresa.

A continuación se calma un poco... vuelve a su puesto y trata de pensar sobre lo que está pasando. La API para clientes se estrenó hace un par de semanas y está pensada para pequeñas aplicaciones que los clientes quieran ejecutar. Pero básicamente se trata de acceso de solo lectura.

— O debería tratarse de solo lectura.

Ignacio se frota los ojos. No se sienta. Acude al baño y se lava la cara con energía. Sale del baño y se para delante de la máquina de Coca-Cola para comprar una, mientras mira con desprecio la máquina de café delicatessen donde los programadores hipsters (esos que utilizan ordenadores Mac), recargan las pilas.

Ahora sí, vuelve a su puesto y, ya seguro de que cuerpo y mente forman una única entidad, se prepara y empieza a analizar de nuevo aquel correo.

— ¿Cómo que "Baltasator"? ¿Se tratará de un rey mago... asesino?

No se resiste a intentar localizar a aquel "hacker", como se hace llamar a sí mismo. Al fin y al cabo, no puedo haber muchos usuarios con ese nombre. Abre un terminal, se conecta al servidor de desarrollo, y prepara un pequeño programa C# con una sentencia linq.

$ ssh igrdgz@cancerbero.infra.sientebien.com
Password: ********
$ csharp
Enter fullscreen mode Exit fullscreen mode

Ignacio lanza CSharp, un intérprete para C# creado por el proyecto Mono, que hace las cosas mucho más dinámicas, sobre todo para un caso como este.

using Sientebien.Infra as Infra;


foreach(string l in Infra.Clientes.Where( c => c.Nombre.StartsWith( "Balt" ) )
        .Select( c => $"{c.Nif} {c.Nombre} {c.Apellidos} {c.IsoAlta} {c.CodigoPais}"))
{
    Console.WriteLine( l );
}
Enter fullscreen mode Exit fullscreen mode
12345345A Baltasar Núñez Bernardez 1981-03-18 ES
55654321W Baltasar Bermúdez Fernández 2015-06-07 GT
11000000C Baltasar Escalante Gómez 2017-10-21 ES
Enter fullscreen mode Exit fullscreen mode

De acuerdo, tenemos a un Guatemalteco, en donde no se ha desplegado la API para los clientes. Otro se dió de alta en 1981 (las fechas están en formato ISO, se recuerda), hace 42 años... si lo hizo a los 20, tendría 62... no parece probable... Y queda este cuyo NIF parece (qué apropiado), un número binario.

— Si compruebo la lista de transacciones del correo del arcángel...

Las siguientes transacciones superan el límite impuesto por la agencia tributaria, y han ocurrido en el plazo de pocos segundos.
12678, 12679, 13887

Veamos:

var trl = new List<Transaccion>() {
                      Db.Transacciones.Get( 12678 ),
                      Db.Transacciones.Get( 12679 ),
                      Db.Transacciones.Get( 13887 ) };

trl.ForEach( tr => Console.WriteLine( tr.NifCliente ) );
Enter fullscreen mode Exit fullscreen mode
11000000C
11000000C
11000000C
Enter fullscreen mode Exit fullscreen mode

¡Había encontrado al perpetrador de aquel desaguisado que pretendía timar al banco 18000 €! De acuerdo, solo quedaba hacer un rollback de la base de datos, tras eliminar aquellas transacciones falsas.

trl.ForEach( tr => tr.Delete() );
SienteBienApi.Commit();
Enter fullscreen mode Exit fullscreen mode

Ahora podía comprobar que realmente aquellos movimientos de su tarjeta habían desaparecido. Podía utilizar la API para clientes, para comprobar que todos los cambios se habían propagado.

var usr = SienteBienApi.Users.Get( "11000000C" );
var tarjeta = usr.Tarjetas[0];

foreach(var m in tarjeta.Movimientos.Movimientos) {
    Console.WriteLine( $"{m.Entidad}: {m.Cantidad} ({m.Concepto})" );
}
Enter fullscreen mode Exit fullscreen mode
Ubisoft: 60 (Compra Far Cry 6)
Retro Radar: 105 (Suscripción Retrogamer)
Aliexpress: 45 (Consola retro)
Aliexpress: 60 (Kit de placa base X99 LGA 2011-3)
Enter fullscreen mode Exit fullscreen mode

Ignacio alzó las cejas en admiración. ¡Eran más de 200 € en...!

— Frikadas. Es un friki de pura cepa...

Los siguientes minutos los dedicó a cumplimentar un formulario para el departamento de fraudes. Le encantaría ver la cara de aquel friki cuando la policía llamase a su puerta.

Pero estaba claro que no podía mantener la conexión con el exterior cortada eternamente. Decidió rellenar con aquel código malicioso una nueva issue en el repositorio Git propio de la compañía. Después decidió echar un ojo al código de antes de tener que volver online con la base de datos. Lo más precavido sería volver en modo de solo lectura, pero sería muy limitante.

$ cd apis/cliente/src/
$ nano ListaMovimientos.cs
Enter fullscreen mode Exit fullscreen mode

Pronto encontró el código responsable de todo aquello. La clase era muy muy sencilla, pues solo tenía que informar de movimientos.

public class ListaMovimientos {
    public ListaMovimientos()
    {
        this.Movimientos = new List<Movimiento>();
    }

    public int Count => this.Movimientos.Count;

    public List<Movimiento> Movimientos {
        get; set;
    }
}
Enter fullscreen mode Exit fullscreen mode

— ¡La propiedad de la lista de movimientos es de lectura y escritura!

Lo que le sorprendía era que además se hubiera soportado (sin pretenderlo, claramente), todo lo necesario para que la lista, en caso de cambiarse, se subiese al banco.

Se dió cuenta de cuál era el cambio a realizar, pero primero había que hacer las cosas bien: completó la issue 420 describiendo aquel ataque, y después una rama en el código, para editar la clase.

$ git checkout -b issue_420
Enter fullscreen mode Exit fullscreen mode
public class ListaMovimientos {
    public ListaMovimientos()
    {
        this.Movimientos = new List<Movimiento>();
    }

    public int Count => this.Movimientos.Count;

    public List<Movimiento> Movimientos {
        get;
    }
}
Enter fullscreen mode Exit fullscreen mode

— De acuerdo, vamos allá...

$ git commit -am"Arregla la debilidad descrita en issue 420"
$ git push
Enter fullscreen mode Exit fullscreen mode

Hizo un pull request de su propia rama para pasar los cambios al repositorio principal. Ya podía borrar la rama.

$ git branch -d issue_420
Enter fullscreen mode Exit fullscreen mode

Los sistemas de integración contínua se encargarían del resto. Ahora podía descansar un poco hasta que la nueva versión de la API estuviese en línea y reconectar el famoso cable...

Ignacio está por fin en el mejor de los sueños cuando suena otra alarma....

22/11/2023 03:56    arcangel@infra.sientebien.com    AVISO - Múltiples ingresos detectados
Enter fullscreen mode Exit fullscreen mode

Ignacio no se lo puede creer... ¿otra vez? Pero cómo es posible...

— ¡¿Es que esta noche es la fiesta de los hacker?!

...y claro, el siguiente correo no le sorprendió...

22/11/2023 03:57    hacker@jajaja.com    ¡No puedes conmigo, mindundi!
Enter fullscreen mode Exit fullscreen mode

¡Vuestros intentos de solucionar esto me dan peeeeeenaaa! ¿Crees que me has detenido? Mira y llora, mindundi:

    using SienteBien.ExternalApi as SienteBienApi


    var usr = SienteBienApi.Users.Get( ..... );
    var tarjeta = usr.Tarjetas[0];

    tarjeta.Movimientos.Movimientos.Clear();
    tarjeta.Movimientos.Movimientos.AddRange(
        new Movimiento[]{
            new Movimiento{ Cantidad = -4000, Concepto = "ja!", Entidad = "jajaja.com" },
            new Movimiento{ Cantidad = -6000, Concepto = "ja!", Entidad = "jajaja.com" },
            new Movimiento{ Cantidad = -8000, Concepto = "ja!", Entidad = "jajaja.com" }
    });

    SienteBienApi.Commit();
Enter fullscreen mode Exit fullscreen mode

¡Gracias mindundis!
Baltasaroff

̣— No me lo puedo creer...

Tras unos cuantos insultos por lo bajo, otros en de viva voz mientras la inocente pantalla pagaba las húmedas consecuencias, y otros improperios que básicamente consistían en recordar con aprecio la familia de aquel desconocido hacker... corrió de nuevo por el pasillo para llegarse hasta el armario de comunicaciones y volver a desconectar EL CABLE. El latiguillo del marcador rojo.

Volvió caminando esta vez, tratando de tranquilizarse, abrió un terminal y se conectó de nuevo al servidor de desarrollo.

— Comprobemos que no es una machada...

    using SienteBien.ExternalApi as SienteBienApi


    var usr = SienteBienApi.Users.Get( ..... );
    var tarjeta = usr.Tarjetas[0];

    tarjeta.Movimientos.Movimientos.ForEach( m =>
        Console.WriteLine( $"{m.Entidad}: {m.Cantidad} ({m.Concepto})" ) );
Enter fullscreen mode Exit fullscreen mode
jaja.com: -4000 (ja!)
jaja.com: -6000 (ja!)
jaja.com: -8000 (ja!)
Enter fullscreen mode Exit fullscreen mode

— Increíble. Pensé que lo había solucionado...

Volvió a abrir el código:

public class ListaMovimientos {
    public ListaMovimientos()
    {
        this.Movimientos = new List<Movimiento>();
    }

    public int Count => this.Movimientos.Count;

    public List<Movimiento> Movimientos {
        get;
    }
}
Enter fullscreen mode Exit fullscreen mode

— ¡Pero si ya no se puede modificar! ¡Le he quitado el maldito set!

Trató de calmarse. Cerró los ojos y respiró hondo varias veces.

Después, inspeccionó el código que le había mandado aquel hacker (o lo que fuera). Lo miró y remiró, hasta que cayó en la cuenta.

— ¡No está modificando la lista, está cambiando los elementos en la lista por otros!

El problema es que el set permite cambiar una lista por otra cualquiera, pero al ser la lista mutable en sí, ¡todavía podía cambiarse de cualquier manera!

Buscó en la documentación de la lista. Una posibilidad era devolver una nueva lista, una copia, pero eso implica iterar por todos los miembros. ¿Habría otra alternativa? Mmm... un método llamó su atención:

IList.AsReadOnly(), que devuelve un objeto que cumple la interfaz IReadOnlyList.

Reabrió y modificó la issue 420. Creó una nueva rama. No eran aquellas horas de la noche el momento para echarle imaginación, así que le llamó a la rama...

$ git checkout -b issue_420_2
Enter fullscreen mode Exit fullscreen mode

Hizo algunas pruebas. Si devolvía la lista a través de AsReadOnly() como IList, entonces los métodos o propiedades que modificaban la lista lanzaban excepciones. Pero si lo hacía a través de IReadOnlyList, entonces no sucedería ni siquiera eso.

Modificó el código en consecuencia. Creó un atributo movimientos que era la verdadera lista. Y entonces modificó la propiedad Movimientos para que devolviera aquella nueva interfaz IReadOnlyList.

public class ListaMovimientos {
    public ListaMovimientos()
    {
        this.movimientos = new List<Movimiento>();
    }

    public int Count => this.movimientos.Count;
    public IReadOnlyList<Movimiento> Movimientos => this.movimientos.AsReadOnly();

    private List<Movimiento> movimientos;
}
Enter fullscreen mode Exit fullscreen mode

No compilaba. Había algunos pequeños problemas a resolver. Como originalmente la propiedad Movimientos era de lectura y escritura, en la API se asignaba libremente en cualquier momento. Pero lo lógico con los nuevos cambios era que se modificara solo al ser creada la lista de movimientos. De hecho, tal y como había quedado ahora la clase ni siquiera se podía llenar la lista de contenido. ¡Había que modificar el constructor!

public class ListaMovimientos {
    public ListaMovimientos(IEnumerable<Movimiento> movimientos)
    {
        this.movimientos = new List<Movimiento>( movimientos );
    }

    public int Count => this.movimientos.Count;
    public IReadOnlyList<Movimiento> Movimientos => this.movimientos.AsReadOnly();

    private List<Movimiento> movimientos;
}
Enter fullscreen mode Exit fullscreen mode

Tuvo que hacer algunas modificaciones en el resto de la API de clientes, de manera que los datos se proveyeran justo en el momento de crear la lista de movimientos, pero eran triviales. ¡Ahora sí, el código era sólido!

$ git commit -am"Soluciona la issue_420_2"
$ git push
Enter fullscreen mode Exit fullscreen mode

Volvió a realizar el ritual de subir el pull request y esperó con impaciencia a que la API volviese a estar compilada y activa. Y sí, volvió a conectar el cable.

Era el momento (por tercera vez en aquella noche) de reclinar la silla y disfrutar de un merecido descanso.

No duró.

Pronto la alarma del correo de urgencia del arcángel avisó, por tercera vez, que algo iba muy mal.

22/11/2023 04:53    arcangel@infra.sientebien.com    AVISO - Múltiples ingresos detectados
Enter fullscreen mode Exit fullscreen mode

Se frotó los ojos. Y miró al infinito. Solo que la pantalla estaba justo delante, mucho más cerca.

Enfocó la mirada en la pantalla. Otra vez el mismo tipo de aviso.

— ¡No puede ser! ¿Otro loco por ahí suelto? ¿O es el mismo? Sí, seguro que es el mismo.

Se levantó y fue caminando tranquilamente al cuarto de comunicaciones (antes de salir, pudo comprobar que llegaba otro correo) y desenchufó EL CABLE. El de la etiqueta roja. De nuevo.

En el programa de correo había efectivamente un nuevo mensaje. No se sorprendió al comprobar el remitente. Bueno, en realidad el "asunto" del mensaje ya era suficientemente expresivo.

22/11/2023 04:54    hacker@jajaja.com    ¡Te pillé otra vez, mindundi!
Enter fullscreen mode Exit fullscreen mode

Ignacio suspiró profundamente. Se echó hacia atrás. Volvió a frotarse los ojos. Y decidió... levantarse. Volver al baño a refrescarse la cara. Coger otro refresco en la máquina. Miró el reloj. ¿Cuándo terminaba su turno? Lo cierto es que ya no quedaba tanto. Eso le tranquilizó.

¿Creías que podrías detenerme con esa tontería de ReadOnly?

¡Soy un gran hacker! ¡Un gran hacker! Y ahora, además rico. ja je ji jo ju... JA JE JI JO JU. Te pongo el código, pringao

    using SienteBien.ExternalApi as SienteBienApi


    var usr = SienteBienApi.Users.Get( ..... );
    var tarjeta = usr.Tarjetas[0];

    foreach(var m in tarjeta.Movimientos.Movimientos) {
        m.Concepto = "Devolución - " + m.Concepto;
        m.Cantidad *= -1000;
    }

    SienteBienApi.Commit();
Enter fullscreen mode Exit fullscreen mode

¡Nunca me pillarás! ¡Gracias gañán!
Baltrópovich

Volvió a ejecutar todos los pasos del ritual que había celebrado ya varias veces. Conectarse al servidor de desarrollo. Abrió de nuevo el intérprete, para cerciorarse de aquello no era mentira. Seguro que no lo era, claro.

var usr = SienteBienApi.Users.Get( "11000000C" );
var tarjeta = usr.Tarjetas[0];

foreach(var m in tarjeta.Movimientos.Movimientos) {
    Console.WriteLine( $"{m.Entidad}: {m.Cantidad} ({m.Concepto})" );
}
Enter fullscreen mode Exit fullscreen mode

Efectivamente, aquel desagradable y molesto desgraciado no mentía. Qué pena.

Ubisoft: -60000 (Devolución - Compra Far Cry 6)
Retro Radar: -105000 (Devolución - Suscripción Retrogamer)
Aliexpress: -45000 (Devolución - Consola retro)
Aliexpress: -60000 (Devolución - Kit de placa base X99 LGA 2011-3)
Enter fullscreen mode Exit fullscreen mode

Esta vez el cambio era mucho más sutil. De hecho, de no ser por el arcángel, por la avaricia de aquel tipo (el arcángel solo advertía de las transacciones por encima de 3000, en uno u otro sentido), y por su afán de notoriedad, aquello podría haber pasado por verdaderas devoluciones si no se hacía ninguna inspección cruzando los datos. ¡Hasta se había molestado en modificar el concepto de cada movimiento!

Estudió el código que le había mandado aquel personaje. Esta vez no modificaba la lista de movimientos, sino los movimientos en sí...

Abrió el código correspondiente a la clase Movimiento.

$ nano Movimiento.cs
Enter fullscreen mode Exit fullscreen mode
public class Movimiento {
    public DateTime CodigoTiempo { get; set; }
    public string Entidad { get; set; }
    public string Concepto { get; set; }
    public double Cantidad { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

A los ojos de su nueva experiencia, aquella clase era un pequeño desastre. Todas las propiedades eran de lectura y escritura (get y set). De todas formas, aquello no iba a suponerle un problema tan grande. Al fin y al cabo, DateTime y el resto de datos (primitivos), eran inmutables, por lo que...

Reabrió la issue 420 y creó una nueva rama, a la que fastidiosamente le asignó un nombre no imaginativo pero sí significativo.

$ git checkout -b issue_420_3
Enter fullscreen mode Exit fullscreen mode

Masculló por lo bajo un par de insultos y remendó rápidamente aquella clase.

public class Movimiento {
    public DateTime CodigoTiempo { get; }
    public string Entidad { get; }
    public string Concepto { get; }
    public double Cantidad { get; }
}
Enter fullscreen mode Exit fullscreen mode

Compiló y... recibió un montón de errores. Es lo que pasa cuando pones una ñapa sin pensar en las consecuencias. Allí donde se creaba un objeto de la clase Movimiento, al asignar sus propiedades el compilador se quejaba...

— Has creado una clase que ni siquiera puede crear objetos que no tenga todo a null, melón.

Recordó aquel libro de autoayuda en el que el autor (psicólogo), explicaba lo importante que era quererse a sí mismo y retiró aquel "melón" de sus pensamientos... y también decidió que sería interesante centrarse en buscar una solución, pero esta vez prestando atención al detalle.

A ver, o bien se asignan todas las propiedades en un constructor (que por ahora no existe), o bien se asignan las propiedades en el momento de crear los objetos de la clase.

Examinó el código de aquella API, todos los objetos Movimiento se creaban utilizando un patrón similar:

var m = new Movimiento();

m.CodigoTiempo = ...;
m.Entidad = ...;
m.Cantidad = ...;
m.Concepto = ...;
Enter fullscreen mode Exit fullscreen mode

Lo decidió en el momento. Iba a utilizar las propiedades que solo se pueden modificar durante la creación del objeto.

public class Movimiento {
    public required DateTime CodigoTiempo { get; init; }
    public required string Entidad { get; init; }
    public required string Concepto { get; init; }
    public required double Cantidad { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

Además, no tenía sentido ningún movimiento faltando alguno de los datos de las propiedades, así que les aplicó a todas el modificador required.

Solo restaba adaptar el par de sitios en los que se creaban movimientos.

var m = new Movimiento {
    CodigoTiempo = ... ,
    Entidad = ... ,
    Cantidad = ... ,
    Concepto = ...
};
Enter fullscreen mode Exit fullscreen mode

Recompiló, hizo el pull request, eliminó la rama y las transacciones... Toda la juerga.

— Me lo estoy pasando pipa con esto.

Volvió al cuarto de comunicaciones a conectar EL CABLE.

Y se tumbó en la silla.

Esta vez, le despertó su jefe.

— ¿Así que una noche tranquila, eh?

Ignacio se pensó mucho la respuesta. Mucho.

— Pues no. Está todo descrito en la issue 420.
— ¿Issue? ¿En el repo de la empresa?
— Ahí. Me voy a dormir.
— ¿Pero entonces qué ha pasado?
— Un hacker quijotesco.

El jefe se dispuso a revisar la issue 420, mientras Ignacio recogía sus cosas. Le echó un último vistazo al correo.

22/11/2023 05:05    hacker@jajaja.com    ¿Dónde estás, mindundi?
22/11/2023 05:08    hacker@jajaja.com    ¡Contesta, mindundi!
22/11/2023 05:11    hacker@jajaja.com    MINDUNDIIIIII
Enter fullscreen mode Exit fullscreen mode

Con una sonrisa de medio lado en la cara, salió de su oficina, y se metió en el ascensor. Se cerraban las puertas cuando oyó a su jefe a través del hall, del área de trabajo, y de su despacho.

— ¡GUTIÉRREEEEEEEZ!

NOTA: Este contenido es totalmente de mi invención, con el objetivo de ilustrar el concepto de inmutabilidad. De hecho, es probable que un banco no funcione así. Cualquier parecido con la realidad es pura coincidencia.

Top comments (6)

Collapse
 
leviarista profile image
Leví Arista

Muy bueno y entretenido!! 👍

Collapse
 
baltasarq profile image
Baltasar García Perez-Schofield

¡Gracias por leer! Me alegro de que guste.

Collapse
 
franciscoortin profile image
Francisco Ortin

Muy bueno, Baltasator :-)

He disfrutado mucho con la historia.

La mutabilidad de objetos, tan propia de la programación imperativa, ha hecho mucho daño como los que indicas en la historia. Una de las medidas tomadas es la introducción de records en C# 9 y la mutación no destructiva en C# 10. La sintaxis de lista de parámetros de los records hace que las propiedades sean init-only y, por tanto, no se puedan modificar. La mutación no destructiva de C# permite, además, la adaptación de los objetos mediante la realización de copias superficiales, sin modificarlos.

Reduciendo la mutabilidad de los objetos, se reducen los errores en el código y se facilita la paralelización de las aplicaciones.

Una forma muy divertida de contarlo. Enhorabuena por el artículo.

Collapse
 
baltasarq profile image
Baltasar García Perez-Schofield

¡Muchas gracias! Me alegro de que te haya gustado. Sí, hay alternativas a las clases como los records, pero bueno, buscaba algo simple y familiar para mostrar las "consecuencias".

Collapse
 
canro91 profile image
Cesar Aguirre

Perfecto para un comic! "Cualquier parecido con la realidad es pura coincidencia" Genial!

Collapse
 
baltasarq profile image
Baltasar García Perez-Schofield

¡Muchas gracias! Me alegro de que te haya gustado. Sí, es una pura invención para repasar la inmutabilidad.