Aaron Ardiri
[Valid RSS] RSS/XML feed
198 entries available (show all)


Internet of Things (IoT)

RIoT Secure AB


In what has been an exhilerating experience, I hope that at least documenting this is of interest.

With the recent release of Pokémon GO version 0.47 and the forced-update, efforts are underway to see what has changed and how the third party developers can once again put their skills to use to capitalize on the success of the game with maps and bots. But it doesn't look good for the community; something drastic has changed, looks like Niantic either got smart or flicked the switch and put a higher setting on strong.codes, the anti-tamper solution they have been using so far.

I would have an educated guess they did both, a change so drastic it doesn't look good.

The hash function candidate was found quickly (0x01B11F24) as were the constants for the PogoHash application that utilizes the Unicorn CPU emulator - if there is one thing that has happened, the reverse engineering team have been static analysis tools to make things easier.

A few patches later, and voila - hash function returns a value.

    :: Hash(buffer, sizeof(buffer)) [iOS code]
    00 00 len=2

But wait a second, it seems regardless what is sent into the function; it always returns the same value, 0x19202a400000000. This started to raise a few suspicions, speculation that this wasn't the hash function but more importantly; they changed the calling sequence as I was speculating in my previous entry. After a few hours; I would suggest they did a pretty darn good job too.

Further investigation, looking at the raw assembler and chatter on discord confirmed this theory.

It seemed that the calling pattern changed; now; they seem to be passing a structure reference (buffer) in the r0 that, with the help of on device debugging tools one of the team was able to set a break point at the function start and dump the contents in memory pointed to.

    (lldb) memory read -c512 0x414194f4
    0x414194f4: 29 01 00 00 08 02 ff ff b8 96 41 41 3f 01 00 00
    0x41419504: 87 49 00 00 5c 95 41 41 28 95 41 41 78 95 41 41
    0x41419514: 5c 95 41 41 00 00 00 00 28 95 41 41 18 98 41 41
    0x41419524: 70 95 41 41 18 64 07 36 28 64 07 36 3c f3 56 28
    0x41419534: 80 9c f2 19 00 00 00 00 00 00 00 00 00 00 00 00
    0x41419544: 54 14 81 07 00 00 00 00 00 00 00 00 00 00 00 00
    0x41419554: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    0x41419564: 00 00 00 00 00 00 00 00 00 00 00 36 00 00 00 00
    0x41419574: a2 ab aa 32 00 00 00 00 00 00 00 00 00 00 00 00
    0x41419584: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    0x41419594: 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00
    0x414195a4: 00 00 00 00 00 00 00 00 80 0b 10 46 5f 00 00 00
    0x414195b4: 00 00 00 00 00 00 00 00 80 0b 10 46 df 0b 10 46

So the next step was to simulate and test this theory - putting a bunch of additional memory logging in place; it was clear that the function being called was selectively reading in twenty-eight uint32_t values from the buffer pointed to by register r0. Most importantly; it didn't crash, giving a strong indication that the values being passed were not pointers to data elsewhere.

One of the first questions was why were only some of the numbers actually being referenced; it seems the size of the buffer is around 192 bytes long. Some of the values look very similar in value to the address being pointed to as well - so; what are they exactly? First thing I wanted to know was how did the function change the buffer, so I added in tracing code for that.

    buffer[] :: changes after
    [0x08] old: 0x414196b8 new 0x583109e1
    [0x0c] old: 0x0000013f new 0x2b18899a
    [0x20] old: 0x4141955c new 0x00000000
    [0x28] old: 0x41419528 new 0xab465392
    [0x2c] old: 0x41419818 new 0xd99c535f
    [0x30] old: 0x41419570 new 0xbf96746a
    [0x34] old: 0x36076418 new 0xb4fb75a5
    [0x40] old: 0x19f29c80 new 0x0000006d
    [0x50] old: 0x07811454 new 0xbf96746a
    [0x54] old: 0x00000000 new 0x73ca4d2d
    [0x60] old: 0x00000000 new 0x00006d6d
    [0x68] old: 0x00000000 new 0x6e1c66f0
    [0x6c] old: 0x00000000 new 0x6c40b32c
    [0x70] old: 0x00000000 new 0xab465392
    [0x74] old: 0x00000000 new 0xd99c535f
    [0x78] old: 0x36000000 new 0x5a516db2
    [0x7c] old: 0x00000000 new 0x7f5994c6
    [0x80] old: 0x32aaaba2 new 0x00000000
    [0x88] old: 0x00000000 new 0x583109e1
    [0x8c] old: 0x00000000 new 0x2b18899a
    [0x90] old: 0x00000000 new 0xb4fb75a5
    [0x94] old: 0x00000000 new 0x2bd1f117
    [0x98] old: 0x00000000 new 0x5a516db2
    [0x9c] old: 0x00000000 new 0x7f5994c6
    [0xa0] old: 0x00000000 new 0x41312878
    [0xa4] old: 0x00000000 new 0x2bd1f117
    [0xa8] old: 0x00000000 new 0x8929848e
    [0xac] old: 0x00000003 new 0x2bd1f117

All of a sudden, the function ends up changing thirty-eight uint32_t values in the source buffer, overwriting some of the values passed in and some additional ones. Additional checking confirmed that the function did not write outside the buffer that we assumed was provided. It then begs the question; if this is the hash function - how does it work?

Is it a replacement of the hash_chunk function? Could it now possibly hash a block of 64, 80, 96 or 112 bytes - are some of the values there as decoys to throw us off? Also; what about the result - the hash was previously an uint64_t value, does the code generate it from four of the known values that have changed? Could the hash be a combination of some of the values like:

    uint64_t hash = ((uint64_t)DWORD(&buffer[0x80]) << 96) | 
                    ((uint64_t)DWORD(&buffer[0x34]) << 64) | 
                    ((uint64_t)DWORD(&buffer[0xa4)] << 32) | 

Or any other combination for that fact - the thing is; we have no idea - without much further investigation it is unclear how the relationship between the previous pointer, length parameters and the return result now correlate to this mysterious structure that is used for input and output.

Coupled with the fact it has been determined that the previously static location hash now has a dynamic element associated with it; once can not even fathom figuring out which values are used for the hash at this level. What was once a fairly easy hook; where there was a known static value to aim for, it has now become a complete nightmare in addition to a new hashing style.

Niantic may finally hold all the secrets here, too many to make reverse engineering feasible.

By all means; at some point there is a pointer to a buffer, length hash request being done - but, it could be way up the calling stack, nested within 300-400 obfuscation jumps, switches and code to lead anyone trying to trace it astray. It surely wont be for the faint hearted - in order to figure out more, on-device debugging and a lot of patience will be required. Rest assured; they wont give up!

Time will tell - for all we knew, a simpler hash function has been implemented and these new hurdles are an elaborate attempt to keep the community distracted or discouraged from pursuing it further. I for one will monitor the discussions and offer assistance where I can but I personally think this is the final nail in the coffin and the cat just found a mischief of mice to feast on.

The source code for the work before the update is available here:

UPDATE: 2016-11-23
It seems there is actually a memory location being read; based on the values passed through to the function. It comes down to the uint32_t value that is passed in at offset 0xbc; if the value is non-zero some calculations are performed and a failed memory read occurs. Will need to look further into it and see what is going on, the value in register r0 points somewhere relative to the stack.

    address: 0x01b129ec
      R00: 0xe3948846 R01: 0x3a3ac3aa R02: 0x00000000 R03* 0x0000a661
      R04: 0x00000000 R05: 0x00000010 R06: 0x0000005f R07: 0xe0006ff8
      R08: 0xffffffff  SB: 0x00000003  SL: 0xe0006d60  FP: 0x02e59970
       IP: 0x00000002  SP: 0xe0006d30  LR: 0x01b129d3  PC* 0x01b129ec
      FLAGS N0 Z0 C1 V0
            eq=0 ne=1 cs=1 cc=0 vs=0 vc=1 hi=1 ls=0 ge=1 lt=0 gt=1 le=0
    opcodes: 40 5c       :: ldrb    r0, [r0, r1]
    Failed on uc_emu_start() with error 
      returned 6: Invalid memory read (UC_ERR_READ_UNMAPPED)

It seems a lot more investigation needs to go into trying to find an appropriate entry point.


advertisement (self plug):
need assistance in an IoT project? contact us for a free consultation.


Building a Raspberry Pi 3 cluster (part 1)
Pokémon GO - Revisiting the "hacking" scene (part 9)

All content provided on this blog is for informational purposes only.
All comments are generated by users and moderated for inappropriateness periodically.
The owner will not be liable for any losses, injuries, or damages from the display or use of this information.