>> Pokémon GO - REVISITING THE "HACKING" SCENE (PART 10)
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
0x19202a400000000
done
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
done
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) |
DWORD(&buffer[0x20]);
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.