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. Muy interesante la parte de XAML sobre todo. No la conocia pero por lo visto las herramientas más famosas para hacer estas tareas ya lo usan:

    http://xamldecompiler.codeplex.com/SourceControl/latest#XAMLDecompiler/MainWindow.xaml.cs

    No estaria de más un poco más de documentación por parte de MS sobre .NetNative y estas cositas…

    Por otra parte sobre el tema de la ofuscacion de código, entiendo que en algunos casos pueda ser importante, pero ni mucho menos es simpre necesario.

    Un saludo

  2. s sound processing capabilities to give you a far more personalised listening experience.
    Mbop Megastore provides both WMA files with DRM and mp3
    with no DRM. There may be plenty of free content on the Internet, but the very best content has been completely produced for you.

  3. The sport Saints Line IV has a DLC bunch of weapons
    and garments named V, which is really a mention of the the initials of Grandtheftauto V.
    The manager, Heavy Silver launched it on Computer for-free, mocking
    the absence of GTA V in the program during the time.

  4. When shopping for a home, it’s also a good idea not
    to share too much information with the seller. Any anomalies should be noted at the beginning so
    they don’t cause problems later on. Choose colors that compliment each other and make you
    feel at home when you’re around them.

  5. I got this web site from my buddy who told me about this site and at the moment this time I am visiting this website and reading
    very informative content at this place.

  6. How does one go about finding another apartment in another state that they have never seen before.
    Back in the «good old days», one could simply run a very basic three line, 3-day rental ad in the local
    paper and expect to receive more than enough leads
    to get that rental occupied. Or they may have to hand back
    all the interest the money has made since the deposit was given.

  7. By giving customers the flexibility to vote posts up or down, Overtime aims to take the significance away from
    follower rely and social standing. Whether you wish to create a
    video of the game-successful purpose scored by your highschool soccer workforce or debate the final out that ended final night
    time’s Yankees game, your video will likely be broadcast to anyone who
    follows the related feed.

  8. Hello, i believe that i noticed you visited my web site so i came to go back
    the choose?.I am attempting to find issues to enhance my web
    site!I suppose its adequate to use a few of your ideas!!

  9. With havin so much written content do you ever
    run into any issues of plagorism or copyright infringement?
    My website has a lot of exclusive content I’ve either
    authored 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 techniques to
    help protect against content from being ripped off? I’d certainly appreciate
    it.

  10. Portanto, anterior a tornar-se obcecado em ganhar dinheiro, primeiro projecto desde uma forma desde permitir que senhor teor fazer essa venda
    PRE a fim de que você.

  11. I definitely wanted to post a simple remark to appreciate you for all of the pleasant tips and tricks you are giving out on this site.
    My long internet search has at the end of the day been rewarded
    with pleasant content to write about with my colleagues.
    I ‘d declare that we readers actually are very fortunate
    to exist in a fabulous place with so many lovely individuals with valuable hints.
    I feel quite lucky to have used your website and
    look forward to so many more exciting times reading here.

    Thanks once again for everything.

  12. Haverá isenção da taxa de inscrição para os
    candidatos que declararem e comprovarem possuir renda familiar per capita” igual ou subalterno ao piso
    salarial vigente sobre estado a São Paulo.

  13. Usually I do not learn article on blogs, but I
    would like to say that this write-up very pressured me to try and do so!
    Your writing taste has been surprised me. Thanks,
    very great post.

  14. Minerar bitcoins na nuvem usando a mineradora FastMining é bem simplória, essa é uma das últimas
    empresas que estou testando mas já estou colocando cá
    pois esta dando um lucro sensacional de praticamente 30% do investimento por mês e isso não se encontra
    em qualquer lugar.

  15. Se em outros esportes encontramos tantos benefícios físicos como no rugby não podemos expressar a mesma coisa dos benefícios emocionais,
    onde este esporte se situa como um dos mas vantajosos.

  16. Great blog! Do you have any tips for aspiring writers? I’m hoping to start my
    own website soon but I’m a little lost on everything. Would you advise starting
    with a free platform like WordPress or go for
    a paid option? There are so many choices out there that I’m totally overwhelmed ..
    Any ideas? Cheers!

  17. Yesterday, while I was at work, my cousin stole my iPad and tested
    to see if it can survive a 30 foot drop, just so she can be a youtube sensation.
    My iPad is now broken and she has 83 views. I know this is entirely off
    topic but I had to share it with someone!

  18. I am not sure where you’re getting your information, however great topic.
    I must spend some time learning much more
    or working out more. Thank you for wonderful info I was looking for
    this information for my mission.

  19. After checking out a handful of the blog articles on your web page, I honestly like your way of
    blogging. I book marked it to my bookmark site list and will be
    checking back soon. Please check out my web site too and
    tell me your opinion.

Dejar un comentario

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