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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
        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<T> Get<T>(string url)
        {
            T obj;
            using (HttpClient client = GetClient())
            {
                try
                {
                    var response = await client.GetAsync(url);
                    if (!response.IsSuccessStatusCode)
                    {
                        var trackSeriesApiError = await HttpContentExtensions.ReadAsAsync<TrackSeriesApiError>(response.Content);
                        throw new TrackSeriesApiException(trackSeriesApiError != null ? trackSeriesApiError.Message : "", response.StatusCode);
                    }
                    obj = await HttpContentExtensions.ReadAsAsync<T>(response.Content);
                }
                catch (Exception)
                {
                    throw new TrackSeriesApiException("", false);
                }
            }
            return obj;
        }
 
        protected async Task<Ttarget> Post<Tsource, Ttarget>(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<TrackSeriesApiError>(response.Content);
                        throw new TrackSeriesApiException(trackSeriesApiError != null ? trackSeriesApiError.Message : "", response.StatusCode);
                    }
                    target = await HttpContentExtensions.ReadAsAsync<Ttarget>(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<UserInfoViewModel> AccountInfo()
        {
            return await Get<UserInfoViewModel>("Account/Info");
        }
 
        public async Task<TokenViewModel> AccountLogin(string username, string password)
        {
            TokenViewModel tokenViewModel = await Post<LoginModel, TokenViewModel>("Account/Login", new LoginModel()
            {
                grant_type = "password",
                UserName = username,
                Password = password
            });
            Token = tokenViewModel.access_token;
            return tokenViewModel;
        }
 
        public async Task<List<Episode>> GetFollowEpisodesComingUp()
        {
            return await Get<List<Episode>>("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!

About the Author

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