How to Root the TVPad

The TVPad M120/M121(S) models can be easily rooted. Please only follow and execute these instructions on your own risk (you might brick your TVPad).

The goal is to get root access via telnet. I will based this howto on the M121S model running firmware 3.06. The same method can be applied for the other models with minimal changes.

The trick is to exploit the firmware upgrade process by providing a custom firmware. To create a custom firmware, obtain a newer or a same version firmware. For the M121S model running 3.06, a quick google search by the names of creatent_usb_update_m121_3311_gvos2.tar provides a newer 3.26 firmware. Make sure to get the the correct firmware for the appropriate model. The firmware tar file can be decompressed and unarchived.

> file creatent_usb_update_M121S_3311_GVOS2.tar
creatent_usb_update_M121S_3311_GVOS2.tar: gzip compressed data, from Unix, last modified: Sun Nov 25 22:56:07 2012

> tar xf creatent_usb_update_M121S_3311_GVOS2.tar
> ls
home  linux.rom  ramdisk.rom  tcboot.rom

> file ramdisk.rom
ramdisk.rom: Linux rev 1.0 ext2 filesystem data, UUID=339d0c45-1592-4ed4-86c4-8382f9520add

The firmware archive contains a couple files. The ones of interest are the ramdisk.rom file and the version files under home. The ramdisk.rom file contains the Linux image to be flashed on the TVPad and is a ext2 filesystem image, which can be mounted and modified. The version files are text files with the firmware version string.

> mkdir tmp
> mount ramdisk.rom tmp
> ls tmp
backup  bin  dev  etc  home  lib  linuxrc  lost+found  mnt  proc  program  root  sbin  storage  sys  tmp  usr

Once mounted, the following files will be modified to provide a known root password, enable telnetd, and customize firmware version number. The firmware version number get checked during the upgrade. The TVPad will only run the upgrade if the version files in the custom firmware is greater than the version on the system.


> cat ./home/program/data/version

> cat ./home/config/version

> cat ./tmp/etc/passwd
nobody:x:1000:1000:Linux User,,,:/home/nobody:/bin/sh
guest:x:1001:1001:Linux User,,,:/home/guest:/bin/sh

For the version files, simply open them in a text editor and change the version string to a higher number than the running version on the TVPad. For the passwd file, the line with the root user and crypt generated password needs to be replaced with a known crypt password. For the curious, the crypt string $1$qacY87Qt$G7PaT7/IPNNaJPBFKhVh1. denotes a MD5 hashed password where qacY87Qt is the salt and G7PaT7/IPNNaJPBFKhVh1. the hashed salted password.

On an Ubuntu system, a crypt MD5 password can be generated with md5pass.

> md5pass mynewpass

> cat ./tmp/etc/passwd
nobody:x:1000:1000:Linux User,,,:/home/nobody:/bin/sh
guest:x:1001:1001:Linux User,,,:/home/guest:/bin/sh

With the new known crypt password for mynewpass in the tmp/etc/passwd file, the root user can now be accessed.

The tmp/etc/init.d/rc.demo file runs when the system boots. Its job is to initialize services on the TVPad. One of the service is telnetd (the telnet daemon). For the 3.26 firmware, telnetd is disabled by default. To enable it, open tmp/etc/init.d/rc.demo in a text editor and search for telnetd. There should be two instances commented out, i.e. # telnetd. Remove the # in front of telnetd and save the file.

After editing all the above files, unmount ramdisk.rom and repackage using the same file name as the downloaded firmware. In this example, creatent_usb_update_m121_3311_gvos2.tar for M121S version 3.26.

> sudo umount tmp
> tar cf - home linux.rom ramdisk.rom tcboot.rom | gzip > creatent_usb_update_m121_3311_gvos2.tar

# note using tar czf creatent_usb_update_m121_3311_gvos2.tar ./* doesn't work. The upgrade process does not like the tar file provided by it.

Copy the repackaged custom firmware tar file to the root of a USB stick. Plug the USB stick into the TVPad, go to Settings -> Upgrade and run. The upgrade process will detect the firmware in USB stick and use it for the upgrade. Once the upgrade process completes, telnet should be accessible.

> telnet
Connected to
Escape character is '^]'.

(none) login: root

BusyBox v1.16.0 (2010-10-27 13:14:24 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ # ls
GVOS_USER_ID  backup        dev           home          linuxrc       mnt           program       sbin          sys           usr
Settings      bin           etc           lib           lost+found    proc          root          storage       tmp           var
~ #

Have fun poking around the TVPad.

My Daily Development Workflow with Emacs and GNU Screen

I like to share my development workflow since it has greatly improve my efficiency as a programmer. Four tools have become indispensable for me as a developer, bash shell, SSH, GNU Screen, and Emacs. I work via remote machines. This has the benefit of a centralized and consistent environment. It provides the flexibility for me to jump from one local thin client to another. At the same time, preconfigured remote development machines save me time from replicating environments in my local boxes.

I start by opening a terminal on my laptop/desktop, SSH into one of the remote machines where I have previously started a GNU Screen session. For the unfamiliar, GNU Screen can multiplex virtual consoles and allow user to run multiple terminals over remote sessions. Within these GNU Screen sessions, I usually have multiple tabs. One tab with an Emacs session, and other tabs with bash shells. These screen sessions persist between SSH sessions. I can disconnect, reconnect from another box and re-attach the same screen sessions and everything would be there. This setup gives the benefit of quickly resuming where I left off. With all the command histories intact, I can pick up my train of thought from the previous work session.

In GNU Screen, I use the following configuration file, ~/.screenrc

# turn off screen's startup message
startup_message off

# tab bar with a clock
hardstatus alwayslastline "%{.bW}%-w%{.rW}%n %t%{-}%+w %=%{..G} %H %{..Y} %m/%d %C%a "

shell -$SHELL

# change default screen escape key to C-z since C-a is one of my favorite key sequence in emacs (go to beginning of line
escape ^za

autodetach on

# I don't like the flashes
vbell off

# one tab with bash, another with emacs
screen -t Shell 0 bash
screen -t Emacs emacs

Some quick tips to get started with GNU Screen assuming the escape key is remapped to C-z

# start a new screen session

# detach/re-attach a screen session
screen -rD

# Some basic Hotkeys to navigate and issue commands within screen
# go to next tab
C-z n

# go to previous tab
C-z p

# skip to tab where num is 0 to 9
C-z num

# jump between last viewed tab and current tab
C-z z

# create a new tab
C-z c

In the Emacs session, I usually have a 3-way screen split. A half/half vertical split of two buffers with the primary source code on the left. The right half of the split is further horizontally split, where the top half contains a secondary source code file and the bottom half with a shell M-x shell.

Emacs 3-way screen split
3-way Emacs buffer split

When I don't need the shell and need more screen space for a secondary source code file reference, I would simply do a 2-way vertical split. Personally, I found it best to use a maximum of 3 splits (50% left, 25% top-right, and 25% bottom-right) and a minimum of 2 splits (50% left and 50% right) because I can quickly tab through two/three buffers. Use the following key sequences in Emacs to setup and navigate the split buffers.

# vertical 50/50 split
M-x 3

# horizontal 50/50 split
M-x 2

# unsplit current buffer
M-x 0

# jump to next buffer (left to right, top to bottom, in cycles)
M-x o

Other useful features I enable in Emacs is iswitchb-mode. This mode changes C-b x, buffer selection command, to substring match buffer names. You don't need to type full name of a buffer, just any substring of the buffer to filter out none matching ones, then C-s to select next or C-r to select previous. I'll leave Emacs tips at that since there are many external resources covering basic Emacs.

We all have our own set of favorite tools and incorporate them into our workflow. Sometimes whatever gets the work done with the least resistant wins, but venturing and learning something new can come with pleasant surprises. If you have never tried Emacs/GNU Screen, give them a try.

Intercept, Analyze, and Modify Puzzle and Dragons Game Traffic


A friend introduced me to Puzzle and Dragons (PAD) and I spent countless hours on the game. For the unfamiliar, you collect monsters and use them to beat dungeons full of other monsters, think Pokemon fused with Bejeweled. The stronger/rarer the monsters you owned, the more difficult dungeons you can tackle for better rewards (more rare monsters). While the game is great fun, it also encourages you to spend money to assist game play, pay-to-win. The game encourages you to buy collection slots, rarer monsters, and various other in-game mechanics. Being curious with a monster collecting crazed, I decide to see if I can break the game. Usually for client/server based games, proxying/analyzing network traffic will reveal a lot about the inner mechanics of the game.

Good Old Days (PAD in HTTP without sneak_dungeon encoding/encryption)

Before the SSL update, PAD exchanges traffic in plain HTTP. The following requests are the ones I found relevant:


The get_player_data Request/Response

The get_player_data command returns a huge json object with all the player's game state. It includes information such as what monsters the player owns, what teams the player has setup, how many collection slots the player has, and etc. With some experiments, I find a few working modifications (note this is before a series of patched that fix some of the exploits -- find out what still works in the later section):

  1. You can have as many monster collection slots as you want without spending gems on them.
  2. You can have any monsters you want in your teams with any stats.
  3. You can change your friends' leader monster to any monster you want, also with any stats.

The following is a snippet of the response return by the get_player_data request:

      {..., 'card': 754, 'clv': 99, 'slv': 36, 'plus': [99, 99, 99, 99], ...}, 
      {..., 'card': 659, 'clv': 99, 'slv': 36, 'plus': [99, 99, 99, 99], ...}
    {"cuid": 1, "exp": 226724, "lv": 47, "slv": 1, "mcnt": 72, "no": 4, "plus": [1, 2, 1, 0]},
    {"cuid": 8, "exp": 86590, "lv": 32, "slv": 1, "mcnt": 19, "no": 79, "plus": [0, 0, 0, 0]},
      {"set_00":[3427, 0, 0, 0, 0]},
      {"set_05":[1966, 715, 53, 897, 2964]},
   "cardMax": 165,
   "cost": 156,

Change cardMax up to 999 for monster box slots. Change cost up to 200 for the maximum team cost. The interesting fields of a friend object are card, clv, slv, plus, where card identifies the friend's leader monster ID (find monster IDs at PuzzleAndDragonX), clv monster level (up to 99), slv monster skill level (up to 36), and plus where the array indicates the monster's bonus HP, ATK, RCV, and awoken skills (I don't remember the order, but maxing at 99 will enable everything for the monster).

New monsters can be added only temporarily to your monster box by appending master objects to the card array in the form:

{"cuid": 1, "exp": 226724, "lv": 47, "slv": 1, "mcnt": 72, "no": 4, "plus": [1, 2, 1, 0]},

cuid identifies this card and needs to be unique. exp can be 0. mcnt can be anything AFAIK. no is the unique ID of the monster. lv, slv, and plus are the same as the friend's object attributes.

Teams can now use the new monsters. The set_0* arrays list the monsters of each team. It references the monsters' cuid defined in the card array.

The sneak_dungeon Request/Response

When the player enters a dungeon, the sneak_dungeon request gets sent. Before GungHo start encryption/encoding this request's response, a player can inspect it and determine whether a specific monster would drop. The player can then decide to enter the dungeon by letting the response through or prevent the client entering the dungeon by rejecting response. By doing so, the player can guarantee a desired drop before spending the stamina for a dungeon. The sneak_dungeon response is as follow:

   "waves": {
      [{"seq": "1",
        "monsters": [{"type": 0, "num": 191, "item": 191, ..., },
                 {"type": 0, "num": 191, "item": 191, ..., }]

The seq specifies monster waves. Each seq has an array of monsters. num identifies the monster. If item has the same value as num, then that monster will drop. If item is 0, then the monster will not drop. If item doesn't match num, then coins will drop. Imagine filtering on these monsters lists to only enter dungeons for rare monster drops. Even better yet, only use stamina if a dungeon guarantees to drop multiple unique monsters. Note this exploit has been patched, the sneak_dungeon response has been encrypted/encoded. However, this request/response is still relevant and can make the game easier/boring.

The save_decks Request/Response

When a player sets up a team to enter a dungeon, the save_decks request gets sent. If the player uses the get_player_data exploits to add temporary monsters in teams, this request will fail and ruin the fun. To by pass the check, simply have the proxy intercept this request and return the following json in the HTTP body:

   "res": 0

For a while before various Gungho patches, the above exploits make PAD predictable and fun. At the time of writing, the iOS version of PAD fixed the cardMax and the sneak_dungeon exploits.

Current Working Exploits

  1. Adding any monsters temporarily to build strong teams still works (exploiting get_player_data and save_decks)
  2. Beat ANY Puzzle and Dragon dungeons with easy in ~5 seconds by exploiting the sneak_dungeon response.


Setup an intermediate gateway, e.g. run a VM with a Linux distribute, enable ip_forwarding, and configure an iptable rule to route all traffic from 443 to 8080. Port 8080 will be running mitmdump (mitmproxy) to intercept and modify traffic.

# assuming a Debian flavor distribution
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8080

# setup a python virtual environment to isolate python installations from system python
apt-get install python-dev libxml2-dev libxstl-dev libffi-dev python-virtualenv
virtualenv --distribute pyenv
source pyenv/bin/activate
pip install mitmproxy

On the iOS/Android device, go to WIFI settings, note down the network info, and changed from DHCP to Static (DHCP mode doesn't allow custom gateways). Enter the noted network info, except Router/Gateway. Router/Gateway should point to the IP address of the server setup as the intermediate gateway.

iPhone Static Network Settings
iPhone static network settings with custom gateway/router

After running mitmdump once, it should create the SSL certificates/keys in ~/.mitmproxy. You'll need to install the certificate on your mobile device. Fire up mitmdump on the intermediate gateway, open the web browser to on your game device, and you should see mitmdump intercepting traffic.


# serve the certificate file over HTTP for phones/tables
cd ~/.mitmproxy
python -m SimpleHTTPServer
Local Webserver Serving Certificate
SimpleHTTPServer serving the mitmproxy certificate
Install Certificate on iPhone
Install mitmproxy certificate on the iPhone
Mitmproxy Intercepting Traffic
Mitmproxy intercepting HTTPS traffic

Once the transparent proxy setup works, mitmdump will need to be scripted for the exploits.

Scripting mitmdump

Create a file named and add the following code snippets. The first part handles the save_decks cheat.

from libmproxy.protocol.http import decoded
from libmproxy.protocol.http import HTTPResponse
from netlib.odict import ODictCaseless
import json

def request(context, flow):
    if 'save_decks' in flow.request.pretty_url(hostheader=True):
        resp = HTTPResponse([1,1], 200, 'OK',
                            ODictCaseless([['Content-Type', 'text/html']]),
                            json.dumps({'res': 0}))

The above request handler intercepts save_decks request and returns success to the PAD client without forwarding the request to the GungHo server.

To get any temporarily monsters/teams setup, intercept and modify the get_player_data response as described earlier. To beat ANY PAD dungeon in ~5 seconds, replay a fixed encrypted/encoded sneak_dungeon response blob to the PAD client.

# helper function to create a max stat master/card object and added to the players card list
def add_card(cards, cuid, card, lvl):
        "cuid": cuid,
        "exp": 0,
        "lv": lvl,
        "slv": 36,
        "mcnt": 3,
        "no": card,
        "plus": [999, 999, 999, 999]

def response(context, flow):
    request_url = flow.request.pretty_url(hostheader=True)
    if 'get_player_data' in request_url \
       or 'sneak_dungeon' in request_url:
        with decoded(flow.response):
            if 'get_player_data' in request_url:
                body = json.loads(flow.response.content)
                if body.get('cost'):
                    # set max team cost
                    body['cost'] = 200

                    cards = body.get('card')
                    # replace the last 5 monsters with custom monsters
                    cards = cards[:-5]
                    add_card(cards, 9000, 754, 99)
                    add_card(cards, 9001, 692, 99)
                    add_card(cards, 9002, 826, 99)
                    add_card(cards, 9003, 1099, 99)
                    add_card(cards, 9004, 387, 99)
                    body['card'] = cards

                    # set the 6th deck to use custom monsters
                    body.get('decks')[5] = {"set_05": [9000, 9001, 9002, 9003, 9004]}
                    flow.response.content = json.dumps(body)
            elif 'sneak_dungeon' in request_url:
                body = json.loads(flow.response.content)
                if body.has_key('e'):
                    # encrypted/encoded blob of the very first PAD dungeon
                    body['e'] = 'B2CALeeULSSTF9i0.r30tQv41C60ppyeQAdJKrNFjnlAOx6wuXtt.N3py7Bl739gKdjT2XLPUSTUTOrY5jTYqvxV2t7RCzFNbvdIGFeEmzlBPI6vAMyy0Q,zK7rLcYSJz,IdfXh9PSMTEOsRXL.awlu6YoWappAMeJOIGLfkh8HoRiWkBRmy0M9py7Bl739gKdicafg9S8aXEcC.4g37iHv38oWeLzENfEOjgAfjumgn9FQ'
                    flow.response.content = json.dumps(body)

How to Beat Any PAD Dungeon in ~5 Seconds

GungHo started encrypted/encode the monster waves in the sneak_dungeon. The e field in the following response contains the monster appearance/drop information.


However, they did not implement any replay attack prevention. If the proxy replaces the e field with the same blob each time, the PAD client gladly accepts it and reruns the same dungeon. Note that when the client enters the dungeon, the server acknowledges and records what monsters will actually get drop. The PAD client does not send back what monsters are defeated. It only lets the server know that the player has beaten the dungeon. As a result, beating any exploited dungeon will lead to server predetermined drops. The blob encoded in the code snippet comes from the very first dungeon of the game. There are three waves of Porings. Hence, any dungeon can be beaten under ~5.

To try out the exploits, start mitmdump in transparent mode with the inline script.

mitmdump -T -s

Taking this one step further, instead of playing the dungeon the player might be able to mimic the dungeon completion request (did not test this theory, might not work). Feel free to mess around with the exchanges and see what else can be done. The same methodology can be applied to any client/server based applications. Hope this has been educational. Have fun PAD'ing.

Beware of Apple ID Phishing For Lost iPhones

Since iOS 7, iOS devices added the Activation Lock feature to discourage theft. It requires iPhone owners to enter their Apple ID logins once their phones get formatted. Without the logins, the formatted iPhones will not boot into the Home Screen. This feature can be enabled by activating Find My iPhone in the iPhone settings.

Recently a friend got her iPhone stolen. Using the Find My iPhone feature, she logged into iCloud and put the phone in Lost Mode. She provided a contact number and a message to be displayed on her lost device in case people would return it. A few days later, she received the following text message on her contact phone.

Phishing Text Message
Phishing text message sent to the contact number of the lost iPhone

The text message offers her a glimpse of hope by promising the location of the lost iPhone. When she opens the provided URL, instead of a website showing the device's location, a website identical to Apple's login page gets shown. For the tech-savy, the site screams of a phishing attack. For my friend and others without a technical background, the website looks legitimate. They will be easily tricked into giving up their logins.

Phishing Website
Apple ID phishing website

If the users enter their Apple ID login, thinking they can view the lost phone's location, the information would get recorded in plaintext by the phishing website. With the devices' associated Apple ID logins, the thief can disable Activation Lock and reuse the iPhone.

Note that the attack is directed at the owner. The phishing text message must have came from the thief, as he is the only one that have the lost contact number. The phishing URL in the message can be tagged with a unique ID tying the device, contact number, and logins the owner enters into the phishing website. With this tie, the thief does not need to guess which stolen login unlocks which stolen iPhone.

Providing a contact number for a lost iPhone can be a good idea if people are honest. If the other party is malevolent, the contact number can be used to trick the owners into giving out their Apple IDs. As a rule of thumb, always be careful when entering sensitive information into websites. If the website is not SSL protected (nowadays all modern browsers show a green lock icon on the address bar to indicate SSL authenticity) don't enter your information. If you have never seen the domain of the website, don't enter your information. Be vigilant with whom you're giving your information.