逆向Mac版扫雷游戏
Mac App Store有一个扫雷的游戏,免费版只能玩初级和中级的级别,像我这种骨灰级玩家,肯定要玩高级级别的:
不过一点击 高级 菜单按钮,就会提示让我购买游戏,贫穷的我只能逆向一下这个app。
一、破解游戏级别限制
首先运行Interface Inspector,这是一个可以查看Mac应用界面元素结构和属性的软件,功能非常强大,运行后附加上扫雷的进程:
然后展开菜单 游戏 –> 新游戏 –> 高级 ,在右边侧边栏可以看到 高级 菜单栏对象的内存地址是0x101331130
:
接下来用lldb附加扫雷的进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Jobs: ~$ lldb -n "Minesweeper Deluxe" (lldb) process attach --name "Minesweeper Deluxe" Process 37343 stopped * thread #1: tid = 0x2b091, 0x00007fff8499cf72 libsystem_kernel.dylib`mach_msg_trap + 10, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP frame #0: 0x00007fff8499cf72 libsystem_kernel.dylib`mach_msg_trap + 10 libsystem_kernel.dylib`mach_msg_trap: -> 0x7fff8499cf72 <+10>: retq 0x7fff8499cf73 <+11>: nop
libsystem_kernel.dylib`mach_msg_overwrite_trap: 0x7fff8499cf74 <+0>: movq %rcx, %r10 0x7fff8499cf77 <+3>: movl $0x1000020, %eax ; imm = 0x1000020
Executable module set to "/Applications/Minesweeper Deluxe.app/Contents/MacOS/Minesweeper Deluxe". Architecture set to: x86_64h-apple-macosx. (lldb)
|
然后使用以下命令获取点击菜单按钮时调用的方法名:
1 2 3 4 5 6 7 8 9 10 11
| (lldb) po 0x101331130 <NSMenuItem: 0x101331130 高级>
(lldb) po [0x101331130 target] <minesweepermacAppDelegate: 0x1013353c0>
(lldb) po [0x101331130 action] 0x0000000100088db1
(lldb) po NSStringFromSelector(0x0000000100088db1) startNewGameExpert:
|
由此可知,点击菜单按钮会调用-[minesweepermacAppDelegate startNewGameExpert:]
方法,用Hopper可以看到该方法的伪代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| void -[minesweepermacAppDelegate startNewGameExpert:](void * self, void * _cmd, void * arg2) { rdx = arg2; rbx = self; r14 = *objc_msgSend; [self dismissLeaderboardView]; rax = [GameState sharedInstance]; rax = [rax fullgame]; if (LOBYTE(rax) != 0x0) { r14 = *objc_msgSend; rax = [GameState sharedInstance]; [rax setDifficulty:0x2]; rax = [GameState sharedInstance]; [rax save]; [rbx->beginner setState:0x0]; [rbx->intermediate setState:0x0]; [rbx->expert setState:0x1]; [rbx->custom setState:0x0]; r15 = *objc_ivar_offset_minesweepermacAppDelegate_window_; [*(rbx + r15) setIsVisible:0x0]; xmm0 = intrinsic_xorps(xmm0, xmm0); rdi = *(rbx + r15); var_40 = intrinsic_movaps(var_40, xmm0); var_30 = 0x408d100000000000; var_28 = 0x4082800000000000; [rdi setFrame:0x1 display:0x0 animate:r8]; [*(rbx + r15) center]; rax = [GameManager sharedGameManager]; [rax runSceneWithID:0x65]; rax = [*(rbx + r15) setIsVisible:0x1]; } else { rax = [rbx unlockFullgame:0x0]; } return; }
|
由代码可知,当-[GameState fullgame]
方法的返回值为YES
的时候,才可以玩高级级别的游戏。
所以可以编写一个插件来hook这个方法,强制返回YES
值。
按照《使用EasySIMBL为Mac应用加载插件》教程里的方法安装EasySIMBL模板,然后用Xcode新建一个EasySIMBL插件工程,工程名为MinesweeperPlugin
:
然后hook-[GameState fullgame]
方法,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @implementation NSObject (GameStateHook)
+ (void)hook_GameState { [self jr_swizzleMethod:@selector(fullgame) withMethod:@selector(hook_fullgame) error:nil]; }
- (BOOL)hook_fullgame { return YES; }
@end
|
编译代码后重新运行游戏,就可以进入高级级别游戏了。
二、破解安全帽数量
扫雷的菜单里还有一个购买安全帽的功能:
在帮助里可以看到安全帽的说明:
使用安全帽
==========
使用 “Alt / Option + 左键点击” 可以保证安全。如果方块下面是雷会被插棋,如果下面不是雷会正常打开。每次会用掉一个安全帽。游戏中随时按下 “Alt / Option“ 键可以看安全帽数量。安全帽用完了可以在商店菜单中添加,也可以通过分享你的成绩获得免费安全帽。
虽然我玩扫雷时不会使用这种作弊的功能,但是在某些情况下还是有用的。比如有时玩到最后会出现2选1的情况,这时如果靠运气点到地雷就太亏了,所以安全帽可以在这种情况下使用。那么顺便把安全帽的数量修改成无限吧。
同理,在Interface Inspector
里查看购买5个安全帽的菜单地址:
再获取菜单调用的方法名:
1 2 3 4 5 6 7 8 9 10 11
| (lldb) po 0x10112c2c0 <NSMenuItem: 0x10112c2c0 5个安全帽>
(lldb) po [0x10112c2c0 target] <minesweepermacAppDelegate: 0x10122b630>
(lldb) po [0x10112c2c0 action] 0x00000001000893a1
(lldb) po NSStringFromSelector(0x00000001000893a1) buy5Robot:
|
在Hopper里查看-[minesweepermacAppDelegate buy5Robot:]
方法的伪代码:
1 2 3 4 5 6 7
| void -[minesweepermacAppDelegate buy5Robot:](void * self, void * _cmd, void * arg2) { rbx = *objc_msgSend; [self dismissLeaderboardView]; rdi = [InAppPurchaseManager getInstance]; rax = [rdi purchase5robot]; return; }
|
再查看-[InAppPurchaseManager purchase5robot]
方法的伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void -[InAppPurchaseManager purchase5robot](void * self, void * _cmd) {
r15 = *(var_68 + r14 * 0x8); rax = [r15 productIdentifier]; rax = [rax isEqualToString:@"com.sg.minesweepermac.robot5"]; if (LOBYTE(rax) != 0x0) { r12 = *objc_msgSend; rbx = [SKPayment paymentWithProduct:r15]; rax = [SKPaymentQueue defaultQueue]; [rax addPayment:rbx]; }
}
|
从代码可知,购买的产品的id是com.sg.minesweepermac.robot5
。
由《一种应用内付费(iap)的破解方法》可知,内购的回调方法为paymentQueue:updatedTransactions:
,在Hopper里搜一下这个方法,可以发现这个方法在InAppPurchaseManager
类里。
接下来使用《Hopper Disassembler批量导出反编译的伪代码》里的方法,反编译出InAppPurchaseManager
类所有方法的伪代码。
打开~/ClassDecompiles/Minesweeper Deluxe/InAppPurchaseManager.m
文件,搜索com.sg.minesweepermac.robot5
,可以很快发现以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| - (void)provideContent:(id)arg2 {
rax = [r14 isEqualToString:@"com.sg.minesweepermac.robot5"]; LODWORD(r13) = 0x5; if (LOBYTE(rax) == 0x0) { rax = [r14 isEqualToString:@"com.sg.minesweepermac.robot15"]; LODWORD(r13) = 0xf; if (LOBYTE(rax) == 0x0) { rax = [r14 isEqualToString:@"com.sg.minesweepermac.robot30"]; LODWORD(r13) = 0x1e; if (LOBYTE(rax) == 0x0) { rax = [r14 isEqualToString:@"com.sg.minesweepermac.robot60"]; LODWORD(r13) = 0x3c; if (LOBYTE(rax) == 0x0) { rax = [r14 isEqualToString:@"com.sg.minesweepermac.robot90"]; LODWORD(r13) = 0x5a; if (LOBYTE(rax) == 0x0) { LODWORD(r13) = LODWORD(0x0); } } } } } rbx = *objc_msgSend; LODWORD(r15) = LODWORD([[GameState sharedInstance] robot]); rax = [GameState sharedInstance]; [rax setRobot:LODWORD(LODWORD(r15) + LODWORD(r13))]; rax = [GameState sharedInstance]; [rax save];
}
|
可以发现,购买成功后通过不同的产品id来增加不同的安全帽数量,然后通过-[GameState setRobot:]
方法保存安全帽的数量。
也就是说,属性robot
储存了安全帽的数量,如果hook了-[GameState robot]
方法,返回999个安全帽的话,就有用不完的安全帽了。
参考代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @implementation NSObject (GameStateHook)
+ (void)hook_GameState { [self jr_swizzleMethod:@selector(robot) withMethod:@selector(hook_robot) error:nil]; }
- (int)hook_robot { return 999; }
@end
|
具体工程代码可以在MinesweeperPlugin下载。
编译工程后,重新运行扫雷,安全帽的数量也变成了999个了: