逆向分析 Surge for Mac 的激活机制
在正文之前,先声明几个问题
- 不讨论逆向和盗版的界限,技术就是技术
- 不讨论 Surge 的定价,网上已经有很多讨论帖
- 不分享相关源代码,如果分享了,整个行为就变成了黑产,与第一条相悖
- 只讨论逆向工程的原理和思路,不过我讲的不一定对,毕竟刚接触逆向第二天
在进行逆向之前,你最好对汇编有点了解,不用精通,知道 寄存器
,SP 指针
,PC
,一些基本的运算符
,这些基本就够了,剩下的水来土掩兵来将挡,用到的时候现学现卖呗
工具
我们要用到的工具大概有 lldb
,class-dump
,Hopper
,Reveal
,dumpdecrypted
,一台越狱的手机
,一台 Mac
上面说的不一定全,比如越狱手机需要装 ssh
,Cycript
,等等,这些网上都有详细教程,我就不多说了
- class-dump: nygard/class-dump
Generate Objective-C headers from Mach-O files.
顾名思义
- Hopper: Hopper
Hopper is a reverse engineering tool for OS X and Linux, that lets you disassemble, and decompile your 32/64bits Intel Mac, Linux, Windows and iOS executables!
翻译成汉语就是这是一个反汇编工具,同样的工具还有 IDA,用法基本一样吧,不过我没用过
不得不提的是,Hopper 的 Lisence 只有 $89.00,已经很良心了,更良心的是不购买的话,免费版可以无限试用,只是每次只能用 30 分钟,对于刚入门的人,我觉得足够了
- Reveal: Reveal
Reveal brings powerful runtime view debugging to iOS developers
Reveal 就不多说了,不光是逆向,正向开发也很有用
- dumpdecrypted: stefanesser/dumpdecrypted
Dumps decrypted iPhone Applications to a file
三年前的祖传代码了,然而还能用。网上很多人说,9.1 SDK 编译的,向下兼容 9.0,言外之意不兼容更低的,实测是 10.0 SDK 编译的,能兼容到 9
实战
1. 砸壳
首先用 dumpdecrypted 砸壳,砸壳教程网上很多,基本都通用,编译好的动态库放到 document 目录的目的是,注入进程之后,仍然以该进程的用户来执行代码,所以能操作的目录只有 document
如果你想砸壳 Extension,也就是后缀 appex
的程序,就稍微麻烦一些了,因为 extension 进程不能独立运行,也没有 document 文件夹的写权限,那就需要改改 dumpdecrypted 的源代码,让他写到一个有权限的目录,编译之后放到 /Library/MobileSubstrate/DynamicLibraries/
,加一个简单的配置文件,当程序启动的时候,这个 dylib 就会被加载,然后砸壳。
具体怎么改,我还不会,以后再研究,反正这次也用不上。
2. class-dump
实际上这步没有也行,因为逆向 Surge 的激活,只是很小的一部分内容,猜也能猜到,肯定关键字有 Activation
3. Reveal
实际上这步没有也是可以的,但是本着物尽其用的原则,我们打开 Reveal,会发现这些 cell 叫做 ResizeCell,这个名字太普通,有可能别的地方也会用到,放弃它。再往上看,有一个 BaseTableView,其实这个名字也很普通,抱着试试看的心态继续下去吧
4. 分析二进制
刚刚砸壳出来的二进制就派上用场了,拖到 Hopper,搜索 BaseTableView,发现并没什么卵用,随便翻翻看,诶似乎有个叫 SurgeMacViewController
的东西,直觉告诉我就是他了。
从这里我们就可以看出来,逆向工程,很多时候就是凭运气,加上细心耐心,再加上作者的疏忽。这些种种的不确定性,正是逆向工程的魅力所在。
再搜索 SurgeMacViewController
,我们可以看到如下方法:
[SurgeMacViewController startDiscover]
[SurgeMacViewController browser:foundPeer:withDiscoveryInfo:]
猜都不用猜,这俩一定是了,上一个是开始发现近场设备,下面的是回调方法。
我们来看 startDiscover
的实现
上半部分(有简化)
-[SurgeMacViewController startDiscover]:
0000000100025564 stp x24, x23, [sp, #-0x40]! ; Objective C Implementation defined at 0x1001dcd60 (instance)
0000000100025580 ldr x20, [x8, #0x98] ; objc_cls_ref_KDProgressHUD,objc_class_KDProgressHUD
0000000100025584 adrp x8, #0x1001fd000
0000000100025588 ldr x1, [x8, #0x140] ; "view",@selector(view)
000000010002558c bl imp___stubs__objc_msgSend
00000001000255a0 ldr x1, [x8, #0x700] ; "showHUDAddedTo:animated:",@selector(showHUDAddedTo:animated:)
可以看出来是启动了一个 Hud,就是传说中的菊花,或者风火轮
下半部分就是重点了
00000001000255e0 ldr x0, [x8, #0x290] ; objc_cls_ref_MCPeerID,_OBJC_CLASS_$_MCPeerID
00000001000255e4 adrp x8, #0x1001fd000
00000001000255e8 ldr x21, [x8, #0xc8] ; "alloc",@selector(alloc)
00000001000255fc ldr x0, [x8, #0x50] ; objc_cls_ref_UIDevice,_OBJC_CLASS_$_UIDevice
0000000100025600 adrp x8, #0x1001fd000
0000000100025604 ldr x1, [x8, #0x4c0] ; "currentDevice",@selector(currentDevice)
000000010002561c ldr x1, [x8, #0x1d0] ; "name",@selector(name)
0000000100025634 ldr x1, [x8, #0x660] ; "initWithDisplayName:",@selector(initWithDisplayName:)
000000010002565c ldr x0, [x8, #0x298] ; objc_cls_ref_MCNearbyServiceBrowser,_OBJC_CLASS_$_MCNearbyServiceBrowser
0000000100025668 adrp x8, #0x1001fe000 ; @selector(setBackgroundColor:)
000000010002566c ldr x1, [x8, #0x668] ; "initWithPeer:serviceType:",@selector(initWithPeer:serviceType:)
0000000100025670 adrp x3, #0x1001cc000
0000000100025674 add x3, x3, #0x150 ; @"surge-activate"
00000001000256a0 ldr x1, [x8, #0x200] ; "setDelegate:",@selector(setDelegate:)
00000001000256b4 ldr x1, [x8, #0x670] ; "startBrowsingForPeers",@selector(startBrowsingForPeers)
同样被我简化过了,翻译成 OC 代码就是
NSString *displayName = [UIDevice currentDevice].name;
self.peer = [[MCPeerID alloc] initWithDisplayName:displayName];
self.serviceBrowser = [[MCNearbyServiceBrowser alloc] initWithPeer:self.peer serviceType:@"surge-activate"];
self.serviceBrowser.delegate = self;
[self.serviceBrowser startBrowsingForPeers];
接着重头戏就在回调方法了
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary<NSString *,NSString *> *)info
0000000100025828 ldr x1, [x8, #0x128] ; "objectForKeyedSubscript:",@selector(objectForKeyedSubscript:)
0000000100025830 add x2, x2, #0x170 ; @"request"
0000000100025838 bl imp___stubs__objc_msgSend
可以看到返回的数据里面,有 request
字段,再往下分析
0000000100025860 adrp x8, #0x1001fe000 ; @selector(setBackgroundColor:)
0000000100025864 ldr x1, [x8, #0x5d8] ; "initWithBase64EncodedString:options:",@selector(initWithBase64EncodedString:options:)
0000000100025868 movz x3, #0x0
000000010002586c mov x2, x23
0000000100025870 bl imp___stubs__objc_msgSend
000000010002587c ldr x1, [x8, #0x160] ; "length",@selector(length)
00000001000258a4 ldr x27, [x8, #0x8b0] ; "dataWithBytes:length:",@selector(dataWithBytes:length:)
00000001000258c8 ldr x1, [x8, #0xfe8] ; "rangeOfData:options:range:",@selector(rangeOfData:options:range:)
00000001000258fc ldr x20, [x8, #0x170] ; "bytes",@selector(bytes)
0000000100025944 ldr x1, [x8, #0x4f8] ; "getBytes:length:",@selector(getBytes:length:)
00000001000259d4 ldr x1, [x8, #0x6a0] ; "initWithPeer:securityIdentity:encryptionPreference:",@selector(initWithPeer:securityIdentity:encryptionPreference:)
00000001000259fc ldr x1, [x8, #0x6a8] ; "invitePeer:toSession:withContext:timeout:",@selector(invitePeer:toSession:withContext:timeout:)
@selector(invitePeer:toSession:withContext:timeout:)
这个就比较重要了,peer,session,context 三个参数填对之后,就坐等激活吧。
5. 调试小技巧
用 lldb
连上手机之后,连接上当前程序之后,执行 image list -o -f
,来查看当前的 ASLR(Address space layout randomization),ASLR 简单说就是一个随机地址偏移,可以用来防范恶意程序对已知地址进行攻击。
那么问题就来了,真实地址是多少呢,当我们执行 image list -o -f
后,可以看到
(lldb) process connect connect://192.168.1.100:1234
Process 767 stopped
* thread #1: tid = 0x830a, 0x00000001990c8c30 libsystem_kernel.dylib`mach_msg_trap + 8, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00000001990c8c30 libsystem_kernel.dylib`mach_msg_trap + 8
libsystem_kernel.dylib`mach_msg_trap:
-> 0x1990c8c30 <+8>: ret
libsystem_kernel.dylib`mach_msg_overwrite_trap:
0x1990c8c34 <+0>: movn x16, #0x1f
0x1990c8c38 <+4>: svc #0x80
0x1990c8c3c <+8>: ret
(lldb) image list -o -f
[ 0] 0x0000000000004000 /var/mobile/Containers/Bundle/Application/8597C7A7-BBDB-4946-AC6F-2AAC1FCAF92F/Surge-iOS.app/Surge-iOS(0x0000000100004000)
0x0000000000004000
就是偏移量,基地址怎么找呢,看上面的汇编代码,以 @selector(initWithBase64EncodedString:options:)
为例,0000000100025864
就是基地址
0000000100025864 ldr x1, [x8, #0x5d8] ; "initWithBase64EncodedString:options:",@selector(initWithBase64EncodedString:options:)
0000000100025868 movz x3, #0x0
000000010002586c mov x2, x23
0000000100025870 bl imp___stubs__objc_msgSend
但是这个地址一般没啥用,就像你断点打到一个方法名上面,看不到什么有价值的东西,紧接着下面的 imp___stubs__objc_msgSend
就有用了,如果你研究过 runtime 的话,会知道所有的 OC 方法,都是通过消息机制调用的,这个消息里面包含了发消息的对象,发消息的名字,消息的内容,它的基地址是 0x0000000100025870
那么答案就出来了,真实地址就是 基地址 + 偏移量,也就是 0x0000000100025870 + 0x0000000000004000 = 0x0000000100029870
,我们在 lldb 打个断点
br s -a 0x0000000100029870
等程序运行到这里的时候,就停下来了,这个时候我们可以用 po (char *)$arg1
,这样的方式,打印出想看的内容,arg1 arg2
依次递增,就是运行时的各个参数,(char *)
只是一个强制类型转换,有必要的时候要适当修改。如果想提前知道 arg1 arg2
代表什么,就随便写行代码,编译成 runtime 的代码,就可以分析出来每个参数的意思了
但是我 C++ 学的不好,这个方法用起来,打印出的结果也是很怪异,还有一种比较简单的方法,这里就需要用到 Theos
了,用 %hook
,hook 这个函数,基本上就可以为所欲为了,想看什么看什么,看完之后,结合上面分析出的几个方法,基本可以理清逻辑
接下来的工作就是分析下如何处理收到的字符串,再把它包装好,丢给 Mac 即可,其他的工作交给 MultipeerConnectivity
就好
In The End
大概方法就是这样了,基本上也是一个完整的逆向流程,从 UI 分析到代码,从类名分析到二进制,再看汇编的实现,最后自己 mock 出一套类似的东西