TrackSeries en OSX: Portar una App de la Windows Store sin tener el código fuente

Ayer me aburrí un poco después del trabajo, así que iba a ponerme a ver una serie, pero siempre me olvido de cuál fue el último capítulo que vi. Hay muchas Apps para eso, pero la que más me gusta es TrackSeriesTV, hecha por unos amigos antiguos compañeros de Microsoft. ¿Qué dónde está el problema? Bueno, que yo estaba con mi portátil en MacOS X y la App es únicamente para Windows Store. La solución estaba clara… tenía que portar la aplicación, esa misma noche, sin documentación y sin acceso al código fuente. ¿Te animas?

Obteniendo acceso a los binarios

Las Apps de la Store se instalan en %ProgramFiles%\WindowsApps aunque por defecto no tenemos acceso a ella. Simplemente tenemos que darnos permiso. Podemos hacerlo en las propiedades del directorio, Security -> Advanced y poniéndonos como Owner.

windowsapps_folder_security

Una vez hecho esto, podemos identificar el directorio de TrackSeries. Pero si entramos, veremos que las cosas han cambiado un poco desde mi charla en 2013 para el CodeMotion. Por aquel entonces las Apps mostraban los XAML en claro, el código JS aparecía tal cuál, podías modificar lo que quisieras, etc. Ahora es un poco más difícil. Para empezar, los ficheros XAML ya no se encuentran en claro, sino en formato xbf. Además, el equipo de TrackSeries no está con el hype de JS (porque sí, es un hype), así que han usado un lenguaje de verdad y en vez de ficheros JS listos para leer, tenemos exes y dlls. Nada que no podamos solucionar, pero nos va a dar un poco más de trabajo. Como dijo Jack el destripador, vayamos por partes.

Descifrando los XBF

Como la entrada va de lo que va, voy a seguir el método de Juán Palomo… así que nos vamos a hacer nuestro “decompilador” de XBF. dotPeek_ReducerEngineLa verdad es que vamos a tomar un mega-atajo y usaremos la dll ReducerEngine que distribuye Microsoft con DotNet Native. Antiguamente había que instalarlo por separado, pero con VS 2015 viene de serie (bueno, se instala cuando se activa la capacidad de desarrollo de aplicaciones universales) así que lo más probable es que ya lo tengáis :) Generalmente el path es: %PROGRAMFILES(X86)%\MSBuild\Microsoft\.NetNative\x64\ilc\tools

Una vez tenemos la librería, nos toca localizar qué partes necesitamos de ella. Para ver la arquitectura de un ensamblado, Visual Studio nos ofrece Object Browser. Esa es la herramienta que deberíamos usar… a menos que tengamos alguna otra. En ese caso, usad siempre la otra. El Object Browser no merece otra cosa más que la muerte. En este caso voy a usar dotPeek de JetBrains por dos razones: funciona y es gratis.

¿Estás decepcionado porque no vamos a hacer nuestro propio decompilador .NET? No me he metido en el fregado porque ya lo hice hace cosa de 4 años, cuando me desperté y dije «voy a hacer un decompilador». Puedes echarle un ojo al código, pero con todos los cambios que ha habido en .NET desde entonces, lo sorprendente sería que decompilase algo más que un hola mundo jeje.

Si navegáis un poco por el ensamblado veréis varias cosas interesantes, pero para lo que nos ocupa ahora mismo, el método ReadXBFFile suena delicioso :D ¿Os imagináis lo que vamos a hacer verdad?

El “problema” es que la clase no es visible, así que nos toca usar un poco de reflectividad. Los pasos serían los siguientes:

  1. Crear un proyecto de consola
  2. Referenciar la librería ReducerEngine
  3. Obtener el Assembly a partir de un tipo público
  4. Instanciar la clase que queremos (Xbf2Xaml)
  5. Invocar el método «ReadXBFFile»
  6. Guardar el resultado del método en alguna parte

A continuación os dejo el código que yo he usado. Básicamente le pasas un directorio como primer argumento y lo escanea en busca de archivos XBF. Para cada uno de ellos invoca al método ReadXBFFile y guarda el resultado en el mismo directorio pero con extensión XAML.

        static void Main(string[] args)
        {
            Console.WriteLine("Xinm v0.1 (Xinm Is Not Magic) - XAML Decompiler");
            if (args.Length == 1 && Directory.Exists(args[0]))
            {
                Array.ForEach(Directory.GetFiles(args[0], "*.xbf"), file =>; File.WriteAllText(Path.GetFileNameWithoutExtension(file) + ".xaml", ReadFile(file)));
                Console.WriteLine("All done!");
            }
            else
            {
                Console.Error.WriteLine("Usage:");
                Console.Error.WriteLine("\t xinm.exe directoryWithXbfs");
            }
        }

        public static string ReadFile(string filePath)
        {
            Console.WriteLine("Decompiling {0}...", Path.GetFileNameWithoutExtension(filePath));
            // Let's get a public type of the assembly
            var analisysEngine = typeof(AnalysisEngine);
            // Now we can ask for any type inside the assembly (yes, even non-public ones)
            var type = analisysEngine.Assembly.GetType("Xbf2Xaml.XBFReader");
            // We've the type, so let's get the method we're interested in
            var method = type.GetMethod("ReadXBFFile");
            // Once we've the type, we need to build the object
            var ctor = type.GetConstructor(new Type[] { typeof(string), analisysEngine });
            // Second parameter is null because we don't need a fully constructed object just to call our method... so I don't bother
            var reader = ctor.Invoke(new[] { filePath, null });
            // Everything is ready... let's call our method!
            return method.Invoke(reader, new object[] { }).ToString();
        }

Ahora ya podemos usar Xinm (Xinm Is Not Magic) para decompilar los XAML. A pesar de lo sencillo de la herramienta, funciona como es debido y obtenemos los XAML en claro :)
xinm

Amén de que el equipo ha tenido bastante sentido del humor (echad un ojo a ErrorPage.xaml), no parece que haya nada en los XAML que necesitemos. Esto es bueno, significa que TrackSeries tiene una buena arquitectura y no están metiendo en XAML lógica de negocio. En otras palabras, hemos perdido el tiempo decompilándolos :P

Si algún día tenéis una tarde aburrida, podéis ir decompilando los XAML de las Apps que tenéis en el ordenador… es sorprendente lo que algunos desarrolladores pueden llegar a poner en ellos xD

A por las DLL

El equipo de TrackSeries ha sido lo suficientemente considerado como para separar su modelo en una DLL (TrackSeries.Web.Api.Models.DLL) y la lógica de negocio en otra (TrackSeries.Logic.DLL). El tercer archivo interesante es TrackSeries.exe, pero está íntimamente ligado a la plataforma, así que con las DLLs tenemos suficiente.

Lo normal aquí es usar un decompilador (yo he vuelto a usar dotPeek)

Parece ser que los programadores de TrackSeries se saltaron el curso de «Defensa contra las artes obscuras» y el código está sin obfuscar. De hecho, la arquitectura es buena y la nomenclatura bastante apropiada… y eso que estamos tratando con codigo decompilado. He tenido que mantener código «humano» mucho peor. Claro que no sé si eso dice mucho a favor de TrackSeries o en contra de algunos humanos… en fin, sigamos.

Llegados a este punto tenemos varias opciones. Podemos hacer ingeniería inversa (que no nos va a costar porque el código generado es muy claro) y crearnos nuestra propia librería, podemos generar un proyecto de VS con el código directamente desde dotPeek o podemos usar ambas DLLs en el proyecto que hagamos.

Recreando TrackSeries.Web.Api.Models.DLL

Esta DLL contiene las clases para almacenar la información que recibiremos y enviaremos a la API. Lo más cómodo es usar dotPeek para generar un proyecto de Visual Studio con el código decompilado. Podemos hacerlo sacando el menú contextual sobre el nombre del ensamblado y seleccionando «Export to Project…»
dotpeek_generate_project
Si os fijáis, veréis que la librería es Portable usando el perfil 151. Que sea portable mola, que sea 151 no tanto. Mono, en el momento de escribir estas líneas, aun no soporta ese perfil… así que tendremos que retocar un poco las cosas.

Ahora nos conviene abrirlo con Xamarin Studio para asegurarnos de que el código sea compatible con Mono, ya que la idea es hacerlo correr en MacOS X. Si lo intentáis compilar tal cual, obtendréis un par de errores:
xamarin_compilation_error
Los problemas que nos vamos a encontrar vienen de BindableBase y de distintas clases NoseQueOb. ¿Solución? Nos cargamos BindableBase y todas las NoseQueOb que veamos, no las vamos a necesitar.

Tras eliminar esos ficheros tenemos una compilación satisfactoria, pero aun sigue usando Framework 4.6, así que nos toca cambiarlo a 4.5:
xamarin_change_framework

Recreando TrackSeries.Logic.DLL

En este caso no voy a generar el proyecto desde dotPeek, sino que crearé el proyecto a mano únicamente con un puñado de clases. Principalmente porque la idea no es hacer una implementación completa, sino solamente una prueba de concepto y segundo porque la implementación de TrackSeries está pensada para ser consumida por una aplicación XAML y no va a ser así.

Vamos a crearnos un proyecto «Class Library (Portable)» en VS haciendo target a Framework 4.5:
vs_trackseries_create

Como prueba de concepto la única funcionalidad que quiero es:

  • Autenticación básica (usuario y contraseña)
  • Información del usuario
  • Obtener lista de próximos capítulos

Si observáis la decompilación que hace dotPeek, podemos aprovechar mucho código. Básicamente necesitamos implementar lo suficiente para que los métodos «AccountInfo», «AccountLogin», «AccountLogout» y «GetFollowEpisodesComingUp» de la clase TrackSeriesApiService puedan funcionar.

El primer paso es implementar (léase, copiar directamente la decompilación de dotPeek) BaseProvider, que es de quién extiende TrackSeriesApiService. Sin embargo, esta depende del paquete NuGet «Microsoft.AspNet.WebApi.Client», así que nos toca instalarlo.
NuGetWebApi
Depende del Framework 4.5, que es precisamente la versión más reciente soportada por Mono, así que todo bien :)

Esto debería solucionar todos los problemas de dependencias externas. Tan solo quería solucionar algunos fallos en BaseProvider. Por algún motivo, dotPeek ha decidido que es mucho más divertido usar HttpClient sin instanciarlo antes… pero al margen de eso el código es usable. Así pues nos quedaríamos con:

using System;
using System.Net.Http;
using System.Threading.Tasks;
using TrackSeries.Exceptions;

namespace TrackSeries.Logic.Services
{
    public class BaseProvider
    {
        protected string _baseUrl;

        protected HttpClient GetClient()
        {
            return GetClient(_baseUrl);
        }

        protected virtual HttpClient GetClient(string baseUrl)
        {
            HttpClient httpClient = new HttpClient();
            Uri uri = new Uri(baseUrl);
            httpClient.BaseAddress = uri;
            return httpClient;
        }

        protected async Task Get(string url)
        {
            T obj;
            using (HttpClient client = GetClient())
            {
                try
                {
                    var response = await client.GetAsync(url);
                    if (!response.IsSuccessStatusCode)
                    {
                        var trackSeriesApiError = await HttpContentExtensions.ReadAsAsync(response.Content);
                        throw new TrackSeriesApiException(trackSeriesApiError != null ? trackSeriesApiError.Message : "", response.StatusCode);
                    }
                    obj = await HttpContentExtensions.ReadAsAsync(response.Content);
                }
                catch (Exception)
                {
                    throw new TrackSeriesApiException("", false);
                }
            }
            return obj;
        }

        protected async Task Post(string url, Tsource content)
        {
            Ttarget target;
            using (HttpClient client = GetClient())
            {
                try
                {
                    var response = await HttpClientExtensions.PostAsJsonAsync(client, url, content);
                    if (!response.IsSuccessStatusCode)
                    {
                        var trackSeriesApiError = await HttpContentExtensions.ReadAsAsync(response.Content);
                        throw new TrackSeriesApiException(trackSeriesApiError != null ? trackSeriesApiError.Message : "", response.StatusCode);
                    }
                    target = await HttpContentExtensions.ReadAsAsync(response.Content);
                }
                catch (Exception)
                {
                    throw new TrackSeriesApiException("", false);
                }
            }
            return target;
        }
    }
}

Con esto ya podemos «implementar» TrackSeriesApiService, que quedaría más o menos así:

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using TrackSeries.Web.Api.Models;
using TrackSeries.Web.Api.Models.Series;

namespace TrackSeries.Logic.Services
{
    public class TrackSeriesApiService : BaseProvider
    {

        public string Token { get; set; }

        public TrackSeriesApiService()
        {
            _baseUrl = "https://api.trackseries.tv/v1/";
        }

        public async Task AccountInfo()
        {
            return await Get("Account/Info");
        }

        public async Task AccountLogin(string username, string password)
        {
            TokenViewModel tokenViewModel = await Post("Account/Login", new LoginModel()
            {
                grant_type = "password",
                UserName = username,
                Password = password
            });
            Token = tokenViewModel.access_token;
            return tokenViewModel;
        }

        public async Task> GetFollowEpisodesComingUp()
        {
            return await Get>("Follow/Episodes/ComingUp");
        }

        protected override HttpClient GetClient(string baseUrl)
        {
            var httpClient = new HttpClient();
            httpClient.BaseAddress = new Uri(baseUrl);
            if (Token != "")
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", Token);
            httpClient.DefaultRequestHeaders.Add("apikey", "Windows8");
            httpClient.DefaultRequestHeaders.Add("User-Agent", "Carballude-Client");
            return httpClient;
        }
    }
}

La parte más interesante es el método GetClient de TrackSeriesApiService, pues es donde aparece el «apikey» (super-originales con el valor Windows8) que usa el cliente para autenticarse con la API. Ya que estaba, añadí un User-Agent distinto para que puedan diferenciar las peticiones que hace mi cliente.

Hasta ahora hemos obtenido el código XAML (aunque no nos haya servido para nada), hemos decompilado y recompilado para una versión inferior del framework la libraría TrackSeries.Web.Api.Models y por último hemos recreado algunas partes de la librería TrackSeries.Logic a modo prueba de concepto. ¿Qué nos queda? Pues hacer una aplicación que haga uso de ambas librerías.

Haciendo nuestra primera llamada a TrackSeries

Para esto no voy a comerme mucho la cabeza. Nos basta con crearnos un proyecto de consola con Xamarin Studio y agregar las dos DLLs que hemos creado en Windows al proyecto. Como ambas librerías hacen target a Framework 4.5, no tendremos problemas para correrlas en MacOS X.

El código del programa podría ser algo estilo:

using System;
using System.Linq;
using TrackSeries.Services;
using TrackSeries.Web.Api.Models;

namespace ConsoleApplication1
{
    class Program
    {

        public Program()
        {
			Console.WriteLine ("Would it be possible to use TrackSeries on Mac?");
			Console.WriteLine ("Let's see...");
            var trackSeriesApi = new TrackSeriesApiService();
            var tokenViewModel = trackSeriesApi.AccountLogin("carballude", "FAKE_PASSWORD").Result;
			Console.WriteLine ("Login user: " + tokenViewModel.UserName + " of type " + tokenViewModel.token_type);
			Console.WriteLine ("Loading upcoming TVShows...");
			var episodes = trackSeriesApi.GetFollowEpisodesComingUp().Result;
			episodes.Take(10).ToList().ForEach(x => Console.WriteLine(x.SerieName + " - " + x.Title));
        }

        static void Main(string[] args)
        {
            new Program();
        }
    }
}

Y podemos comprobar que efectivamente funciona:
Screen Shot 2015-09-13 at 1.13.58 AM

Conclusión

Creo que todos teníamos claro que, a pesar de las apariencias, Windows no ofrece ningún tipo de protección para acceder a los binarios de las Apps de la Windows Store. Lo que quizá muchos no tienen tan claro, es la sencillez con la que las aplicaciones pueden ser decompiladas, diseccionadas e incluso regenerar su código fuente con un par de clicks usando únicamente herramientas gratuitas.

Esto también sirve como prueba de que si nos preocupamos mínimamente por separar la lógica de negocio de la capa de presentación de nuestras Apps, la migración a otras plataformas se convierte en algo tremendamente sencillo.

La próxima vez que hagáis una aplicación para la Windows Store recordad no dejar en el código cosas que no queráis que otros vean porque… seguramente alguien las verá.

Happy hacking!

Por Carballude

Me llamo Pablo Carballude González, soy graduado en computación con master en HCI y Seguridad Informática. Actualmente trabajo para Amazon en Seattle como Software Developer Engineer. Soy de esas personas que no saben si los textos autobiográficos deben ser en primera o tercera persona. Lo intenté en segunda, pero no le entendí nada :P

281 comentarios

  1. After looking over a number of the blog articles on your web site, I seriously
    appreciate your way of blogging. I added it to
    my bookmark website list and will be checking
    back in the near future. Please check out my website as well and let me know
    your opinion.

  2. Please let me know if you’re looking for a article writer for your
    site. You have some really great articles and I believe I would be
    a good asset. If you ever want to take some of the load off, I’d really like to write some content for your blog in exchange for a link back to
    mine. Please send me an email if interested. Thanks!

  3. Magnificent beat ! I wish to apprentice while you amend your web site, how can i subscribe for
    a blog web site? The account helped me a acceptable deal.
    I were tiny bit familiar of this your broadcast provided brilliant transparent concept

  4. What i don’t realize is in reality how you are now not actually much more smartly-liked than you might
    be now. You’re very intelligent. You recognize therefore significantly relating to this subject,
    produced me personally imagine it from a lot of numerous angles.
    Its like women and men aren’t interested except it is something to do with Lady gaga!

    Your own stuffs great. Always handle it up!

  5. With havin so much content do you ever run into any issues
    of plagorism or copyright violation? My website has a lot
    of unique content I’ve either created myself or outsourced but it appears a lot of it is popping it up all over the internet without my authorization. Do you
    know any solutions to help stop content from being stolen? I’d definitely
    appreciate it.

  6. With havin so much content do you ever run into any problems of plagorism or copyright violation? My blog has a lot of exclusive content I’ve either authored myself or outsourced but it seems a lot
    of it is popping it up all over the web without my permission. Do
    you know any ways to help protect against content from being ripped off?
    I’d certainly appreciate it.

  7. Wagering games that are extremely attached with the hearts of Indonesian individuals
    are known as s1288 cockfight gambling. Looking at Indonesia’s historical
    record, cockfighting games have existed even since the days of the dominion. This activity is not simply entertainment but is also part of a culture passed down through years of Indonesian ancestors.

    Regrettably, although this activity will be a cultural
    heritage of thousands of years, the particular Indonesian government does not really discriminate in enforcing typically
    the law until finally it is extremely difficult to find a place to play
    cockfighting in Indonesia.

    Fortunately technology invented typically the internet because in typically
    the modern era like you can now again access exciting video games through an internet
    relationship. Enough with the capital of electronic devices such as laptop
    computers, computers or smartphones linked to the internet,
    you can immediately enjoy the excitement of playing.

    Presently there are indeed many online gambling sites that offer similar provides on the internet,
    yet however not all of these types of sites can be trusted.
    Only a handful regarding sites can provide professional services which can be worthy of being obtained by consumers as provided by the
    town of S1288 Sabung Chicken breast.

    S1288 or better called S128 has been present for years serving every fan of online game cockfight gambling throughout Asia, even the world.
    Hundreds of people are linked every week in order to enjoy every
    match that is presented and you can watch the particular
    match directly through the particular available live streaming.

    Presently there are many interesting functions that are owned
    by s1288 as the finest chicken gambling bookie
    in Asia. Listed here are interesting functions that you need to know:

    Live streaming!
    The first advantage you may get from playing cockfighting
    may be the availability of live buffering chicken battles
    that are served in HD or even high definition quality.

    I think very clear so that a person won’t miss a solitary bit of
    excitement employing S1288 as your online gambling partner.

    Various competitions are available
    There are several tournaments that you take part in constantly so no need to be scared you won’t get a online game because with s1288
    poultry matches will always be present every single
    day and you will enjoy the variety of games you want.

    Attractive Bonuses and Promotions
    Customer comfort is a new top priority for people. Therefore,
    this site has provided many interesting bonuses in addition to promos that you can follow as an official member.
    These bonuses plus promos are given because thanks from s1288 in order to customers for choosing all of them as
    partners to perform online gambling.

    How to be able to Play Chicken Gambling about S1288
    All you want to do first prior to starting the game is usually
    to register yourself as a member on the particular
    Betberry site. It’s simple, visit the official Betberry website and your
    sign up menu. Complete the registration process and you could immediately obtain a user ID plus password that you will later use to
    log into the game.

  8. Betberry is the official and trusted agent in Indonesia
    that gives soccer betting, on line casino, joker123 shooting, s1288 penis fighting
    and online slot machines. Betberry is one associated
    with the sites that has been established since 2014.
    With a total associated with hundreds of thousands more members throughout Indonesia,
    Betberry online betting is very popular ranging from
    young adults to adults. Given the amount of enthusiasm of people, we all together created an established blog site of official2018 to be able to provide more
    information in addition to knowledge about online gambling betting and how to win bets.

    BETBERRY SITE IS TECHNICALLY RELIABLE gambling agent
    given that 2011
    Being a trusted on the internet betting site with the greatest number of members in Asia, has
    a great official gambling site that will has been established considering that 2011, namely Betberry.
    Betberry provides various games these kinds of as:

    Maxbet Ball Gambling Games,
    Sbobet Soccer Betting Game,
    Online Casino Video games,
    Joker123 game shoot fish,
    The S1288 cock combat game,
    And many more.
    To accessibility Betberry it is likewise very simple to access, just about all you have to do is copy the betberry url address
    and make the link in a new tab. Not simply convenience, various bonuses
    offered by Betberry are also extremely various and interesting.
    Properly, one of the best selling bonuses is the welcome bonus of 20% for sportsbook games.

    Only by looking into making the first deposit, your IDENTITY will immediately be added to 20% of the first deposit.
    How to declare this bonus is pretty simple, you
    only have to validate with CS, then a reward will
    be filled. Regarding withdrawals, it’s not too difficult you only have
    got to reach WIN 6x total (bonus + deposit), you can win WD with
    the 20% added bonus that we have provided. Very exciting promo right?

    MISSION REGARDING BETBERRY AGENTS FOR TECHNOLOGY OF Gamblers
    Betberry that has been going through the globe of online gambling with regard to 8 years.
    It always wants to give the best for each and every member or potential new members who want to sign up for.
    With Betberry’s trusted on the internet betting site, consequently , some of the leading sectors based on Betberry.
    The primary headlines or focus to be improved this season are usually as follows:

    Interesting Promos: Betberry is confident of which attractive gambling promos will certainly make players
    feel more at ease playing.
    Faster DP and WD services: Betberry usually offers faster processing services and in less than 3 minutes you can enjoy and withdraw
    their cash.
    day to day service: 24 hour non-stop service, always providing the very best service and assisted by simply
    professional staff.
    Transactions together with local banks: By providing a local bank.

    Then it will be easier for you when making a transaction.
    Latest security technology: Your protection and privacy are
    the golden gates needed in order to maintain 100% as a lot as possible for your own convenience.

    The noble quest of the Betberry agent, of course, is based on rules and regulations.
    Which incidentally has been with us within the gambling world since since the beginning.

    That is, producing the world of wagering among the best solutions for
    the particular public in the planet. To play finance without
    having breaking the rules. The particular best fix is to offer the best online betting game facility.
    In this way all activities and transactions must be done online
    without breaking the rules.

  9. Bastante sección de contenido. Acabo de stectable en su sitio web y en el capital de la adhesión para Jones que yo
    adquirir realidad disfrutado cuenta de sus entradas de blog.
    de todos Modos se suscribirse a su aumentar y aún no logro acceder constantemente rápido.

  10. Simplemente debo decirle que ha escrito un articulo genial y que realmente disfrute leyendo.
    Estoy maravillado por lo bien que expuso su material y presento sus puntos de vista.
    Gracias.

  11. Thanks a bunch for sharing this with all folks you really recognise what you are talking about!
    Bookmarked. Please also discuss with my site =). We may have a
    hyperlink alternate agreement between us

  12. May I simply say what a comfort to find somebody who actually knows what they are discussing on the net.

    You actually realize how to bring an issue to light and make it
    important. More people have to look at this and understand this side of your story.
    It’s surprising you are not more popular because you certainly have the gift.

  13. Pingback: best snow blowers
  14. Pingback: ipab tv zgemma
  15. Pingback: lg televisori
  16. Pingback: deals on tvs uk
  17. Pingback: Full File
  18. Pingback: click homepage
  19. Pingback: Car Paint Sprayer
  20. Pingback: contigo sippy cups
  21. Pingback: Pvusnocht official
  22. Pingback: hatsan 95 vortex

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *