Chess.com: Hacking Rated Puzzles

Hacking Rated Puzzles in Chess.com’s iOS App

Chess.com's iOS app has a section for rated puzzles — these are chess puzzles that affect a player's puzzle rating.

Chess.com doesn’t use its servers to validate answers to puzzles. That means the app just tells the server whether or not the user submitted the correct answer, and so the logic is vulnerable to reverse engineering.

There are three ways to hack the Rated Puzzles, and they can all be combined to make an even better hack.

  1. View hints without dropping your rating

  2. Mark the answer as correct by submitting only the first correct move instead of the entire sequence of moves

  3. Mark the answer as correct by submitting just 1 move, even if it is the wrong move.

1. View Hints Without Affecting Rating

Take a look at the static binary of the Chess.com iOS app, you should find this procedure:

[CHTactic setHintUsed:]:

000000010034b5dc adrp x8, #0x101b66000

000000010034b5e0 ldrsw x8, [x8, #0x804]

000000010034b5e4 strb w2, [x0, x8]

000000010034b5e8 ret


The binary on your phone might be a bit different, but I suspect any version of the app will have something similar to assembly above.

If you set a breakpoint here using LLDB, you should see that this procedure is called whenever a hint is requested. Great — now we know where to patch the binary!

The line that is responsible for storing the “hint used” flag is:

000000010034b5e4 strb w2, [x0, x8]


So we can patch this out with a nop instruction in LLDB:

memory write 0x10034b5e4 1F 20 03 D5


Now if you use LLDB to take a look at the assembly you should see:

[CHTactic setHintUsed:]:

000000010034b5dc adrp x8, #0x101b66000

000000010034b5e0 ldrsw x8, [x8, #0x804]

000000010034b5e4 nop

000000010034b5e8 ret


You can now view as many hints as you want, and the app will never remember that you viewed a hint. That means when it comes time for the app to submit your answer to the servers it won’t know to tell the servers that you used hints!

Done.

2. Reduce Solution to One Move

Take a look at the static binary of the Chess.com iOS app, you should find a procedure named -[CHBoardView onMoveMadeByUserInTactic:lastMoveBlock:correctBlock:incorrectBlock:]:, and in this procedure you should find something along the lines of:

00000001001c2648 bl imp___stubs__objc_msgSend

00000001001c264c mov x26, x0

00000001001c2650 mov x0, x27

00000001001c2654 bl imp___stubs__objc_release

00000001001c2658 cmp x21, x26

00000001001c265c b.ne loc_1001c2784


By stepping through the assembly and observing the behavior at runtime it can be determined that this assembly code does the following:

  • 00000001001c2648: Calls some function, and stores a result in the register x0

  • 00000001001c264c: Moves the value in x0 to the register x26

  • 00000001001c2658: Compares the value in x26 to the value in the register x21

  • 00000001001c265c: If the value in x26 does not equal the value in x21, then branch to whatever is at loc_1001c2784.

It turns out that the cmp instruction is checking to see if all of the moves in the correct sequence have been made.

Great — now we know where to patch the binary!

By using the nop instruction, we can patch out the logic that makes the app think there are more moves remaining:

memory write 0x1001c265c 1F 20 03 D5

After applying this patch you should be able to make the first correct move (out of a possible sequence of many moves) and the app will consider the puzzle solved and pass the message along to the server.

Done.

3. Incorrect Move = Correct Solution

Take a look at the static binary of the Chess.com iOS app, in the same procedure from step 2 you will find this assembly:

00000001001c25d4 ldr x1, [x8, #0x718]

00000001001c25d8 mov x0, x24

00000001001c25dc mov x2, x25

00000001001c25e0 bl imp___stubs__objc_msgSend

00000001001c25e4 cbz w0, loc_1001c26e8

I used Hopper to look at the static binary, and Hopper is able to determine that the code above branches to a function named compareSANMove (SAN might stand for Solution in Algabraic Notation).

In any case, by stepping through the assembly at runtime it can be determined that:

  • 00000001001c25d4: Prepares a call to compareSANMove

  • 00000001001c25e0: Returns a value that can be accessed at the register w0

  • 00000001001c25e4: Branch to loc_1001c26e8 if the value in the register w0 is equal to zero.

It turns out that cbz is checking to see if the incorrect move was made.

Great — now we know where to patch the binary!

Again, by using the nop instruction we can patch out the logic that makes the app think an incorrect move was made:

memory write 0x1001c25e4 1F 20 03 D5

After applying this patch you should be able to make any move, no matter how obviously incorrect, and Chess.com’s iOS app will think you made the correct move and eventually tell the servers that you made a correct move. (Note that you should use this in combination with hack 2 to prevent weird scenarios on the chess board).

Done.

Conclusion

By patching the binary of Chess.com’s iOS app we were able to change the app logic so that any single move is considered a correct solution and the servers would boost our public puzzle rating — even for puzzles that require 10 correct moves in a row.

The main vulnerability with Chess.com’s iOS app is that logic is being ran on the app side, and so it is vulnerable to reverse engineering. To be fair, I think Chess.com does this to give chess players a better experience. Otherwise, players would have to wait for a server call to let them make their next move which is not always ideal.

In any case, there is no excuse for Chess.com sending the solutions to puzzles in plain text format! That might be why Chess.com has users with puzzle ratings over 60,000!

The json below is how the iOS app gets a chess puzzle's info directly from their servers:

"tactics_problem":{

"is_rating_provisional":false,

"id":1310581,

"initial_fen":"3r1b2/7r/1N1p2k1/2nPp3/2Q3pq/2P1B3/PP4PP/R4RK1 w - - 0 29",

"clean_move_string":"1. Na4 Qxh2+ 2. Kf2 Rf7+ 3. Ke1 Qg3+ 4. Kd1 Rxf1+ 5. Qxf1 Qxe3 ",

"attempt_count":587,

"passed_count":204,"rating":2910,"average_seconds":211,

"user_moves_first":false,

"themes":[],

"user_position":2,

"move_count":5

}

clean_move_string is the answer to this tactic, and anyone can read it with a simple proxy!

Bonus

You can use the script below, along with Frida and a jailbroken iOS device to apply the patches described above. To use this, you'll most likely have to update the hex addresses to match your app version's static binary (I did this on version 4.0.2 (3) of the Chess.com's iOS app).