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.