Contents

工具小子的教程2-破解一个 Mac 程序

这不是笔记,这是日记。简单说明一下要破解一个OSX(唔……马上就要叫macOS了)程序所需要的知(tao)识(lu)和小工具。毕竟作为工具小子,会用工具就是成功的一半。

0xFF 入门知识

  • x86汇编 总该会一点的。印一个cheat sheet对着看就行。
  • C语言 需要有一定了解,毕竟反汇编结果都是以伪C语言形式呈现。
  • 常识 注册按钮英文是register,激活叫activate,被捉奸是revoked,等等。
  • 套路 看完

0x00 工具

  • IDA 一键反编译。静态分析大杀器。
  • Hopper Disassembler OS X 下的反汇编软件,下试用版就行。对 Obj-C 有独特的理解,毕竟时髦
  • unsign(可选) 匿名大神的小工具,强制移除程序签名。mac下数字签名是几乎所有程序都有的,下文会讲两个绕过签名的方式,这是其中之一。

0x01 字符串查找

这里选取的软件是 OS X 下著名的“网络加速工具” Surge。实战是宝宝最喜欢的吼吼吼

从官方网站下载下来的软件是一个 Surge.app 的文件夹,名为[name].app 的文件夹是OS X软件的组织方式。在 Surge.app/Contents/MacOS/ 下找到软件真正的可执行文件。分别载入 Hopper和IDA。

Shift-F12打开IDA字符串窗口, Ctrl-F 搜索 register,一不小心就找到一个重要的东西 registerButtonPressed

/zh/2016/osx-crack/00.png

0x02 查找引用

想想大二学过的MVC也知道,这是一个按钮事件,还是注册按钮的。绑定它上面的函数一定是注册相关的关键函数。双击进入反汇编窗口在这里查找数据引用。说一点IDA这里的好,所有的字符串名字都被自动命名为与字符串内容相关的名字比如 sel_registerButtonPressed 。非常直观。虽然反编译出来的代码里Obj-C引入的 CFString 字符串引用都瞎得不能看……可还是很好用哒!

注意,这里不能使用 x 键直接搜索这个字符串的引用。这里我走了点弯路。开始我以为它跳到的函数一定是注册相关的函数,跳到了xref直接把变量命名为了 registerButtonPressed ,后来我又看了看逻辑发现是个死胡同的时候,仔细看了看调用语句,是这样一句。

# IDA
__text:000000010009E6FC                 lea     rax, registerButtonPressed
# Hopper
000000010009e6fc         lea        rax, qword [ds:sub_10009e785]

从命名规则来看, registerButtonPressed 这个字符串被当成了一个函数呀!虽然这个函数也跟注册有点关系(点击注册信息后获取信息的函数),可刚刚我们看到它是个字符串诶。直到这里,我才注意到 code reference 和 data reference 的区别,关于这个话题这里有一篇详细的文章1。总之,大体区别是,我们在一行代码上点击 x 查找 shu ju yin yong,是对当前汇编语句的“参数”(operand)进行的查找,而data reference是对这个语句所在内存地址的引用的查找,我平时习惯在函数名上弹xref窗,所以才看不出区别。但是在这个字符串上,应该查看的是数据引用。数据引用的查看,可以用IDA的 Cross Reference subview。汇编注释里,默认也显示一个。显示的数量可以在设置里改。详见刚刚提到的文章1

/zh/2016/osx-crack/01.png

0x03 修改关键函数

跳转过去后,发现这个函数被绑定到 __WindowController_registerButtonPressed__ 里,而那个控制器里向某个变量里储存了一个函数。

================ B E G I N N I N G   O F   P R O C E D U R E ================



                     -[WindowController registerButtonPressed:]:
000000010009e2bd         push       rbp                                         ; Objective C Implementation defined at 0x10029a338 (instance)
000000010009e2be         mov        rbp, rsp
000000010009e2c1         sub        rsp, 0x30
000000010009e2c5         mov        rax, qword [ds:imp___got___NSConcreteStackBlock]
000000010009e2cc         mov        qword [ss:rbp+var_28], rax
000000010009e2d0         mov        dword [ss:rbp+var_20], 0xc2000000
000000010009e2d7         mov        dword [ss:rbp+var_1C], 0x0
000000010009e2de         lea        rax, qword [ds:sub_10009e322] ; 关键函数(因为整个方法里面只有这个变量类型是函数 ˊ_>ˋ )
000000010009e2e5         mov        qword [ss:rbp+var_18], rax
000000010009e2e9         lea        rax, qword [ds:0x100257d20]
000000010009e2f0         mov        qword [ss:rbp+var_10], rax
000000010009e2f4         call       qword [ds:imp___got__objc_retain]
000000010009e2fa         mov        qword [ss:rbp+var_8], rax
000000010009e2fe         mov        rsi, qword [ds:0x1002ae0b0]                 ; @selector(refresh:), argument "selector" for method imp___got__objc_msgSend
000000010009e305         lea        rdx, qword [ss:rbp+var_28]
000000010009e309         mov        rdi, rax                                    ; argument "instance" for method imp___got__objc_msgSend
000000010009e30c         call       qword [ds:imp___got__objc_msgSend]
000000010009e312         mov        rdi, qword [ss:rbp+var_8]
000000010009e316         call       qword [ds:imp___got__objc_release]
000000010009e31c         add        rsp, 0x30
000000010009e320         pop        rbp
000000010009e321         ret
                        ; endp

这个函数就是点击按钮以后触发的事件。跟进去以后就是啦!

__text:000000010009E322 ; =============== S U B R O U T I N E =======================================
__text:000000010009E322
__text:000000010009E322 ; Attributes: bp-based frame
__text:000000010009E322
__text:000000010009E322 ked_doActivate  proc near               ; DATA XREF: -[WindowController registerButtonPressed:]+21o
__text:000000010009E322
__text:000000010009E322 var_48          = qword ptr -48h
__text:000000010009E322 var_40          = dword ptr -40h
__text:000000010009E322 var_3C          = dword ptr -3Ch
__text:000000010009E322 var_38          = qword ptr -38h
__text:000000010009E322 var_30          = qword ptr -30h
__text:000000010009E322 var_28          = qword ptr -28h
__text:000000010009E322
__text:000000010009E322                 push    rbp
__text:000000010009E323                 ...
__text:000000010009E336                 call    ked_getValid   ; 让这个函数的返回值为2!
__text:000000010009E33B                 cmp     eax, 2
__text:000000010009E33E                 jnz     loc_10009E410  ; 不是2就死了
__text:000000010009E344                 ...
__text:000000010009E39C                 mov     rsi, cs:selRef_initWithTitle_message_cancelButtonTitle_cancelAction_
__text:000000010009E3A3                 lea     rdx, cfstr_SurgeHasBeenAc ; "Surge has been activated. Thanks for your purchase. Enjoy now!"
__text:000000010009E3AA                 lea     r8, cfstr_Ok    ; "OK"
__text:000000010009E3B1                 lea     r9, [rbp+var_48]
__text:000000010009E3B5                 xor     ecx, ecx
__text:000000010009E3B7                 mov     rdi, rbx
__text:000000010009E3BA                 call    r12 ; _objc_msgSend

0x04 应用签名

OS X的软件“安全”这一点应该毋庸置疑,因为默认的设置下,苹果系统只允许花一百美元一年在苹果进行防沉迷认证的程序员写的程序运行在系统上,一旦程序员走火入魔发放恶意软件,向苹果举报,它就会被revoked,系统的GateKeeper就会阻止程序运行2。而且系统会无条件,没有白名单的拒绝被篡改后签名失效的程序和虚假签名的程序。算是一个不大不小的问题。解决办法,不是改签就是删掉签名咯。

0x0401 删除数字签名

这个小程序 unsign 可以删除一个 mach-o binary里面的签名认证,不过缺点是不能重新签名了……破坏性蛮强的。mac虽然拒绝假签名,但我们穷逼就是没钱苹果也得让着,没有签名的程序手动添加白名单凑合凑合就能用。用这个程序把应用里面所有的二进制文件的签名验证全部删除就可以了,赠送一句命令,删除app里面其他所有的验证文件。

uns(){ ~/unsign "$1";mv "$1" ~ ; mv "$1.unsigned" "$1";}
uns 'Surge.app/Contents/MacOS/Surge'
uns 'Surge.app/Contents/Applications/Surge Dashboard.app/Contents/MacOS/Surge Dashboard'
uns 'Surge.app/Contents/Applications/surge-cli'
find . -name _CodeSignature | xargs -I% rm -R "%"

0x0402 修改数字签名

这个方法本来是苹果为被黑阔暗算的小苦逼们准备的,可以覆盖程序原来的签名并写入新的验证文件。照猫画虎的给surge写了一个批量签名小脚本。使用要求:一百美元一年的mac developer ID(),系统安装任意版本的XCode。

#!/bin/bash

# https://jbavari.github.io/blog/2015/08/14/codesigning-electron-applications/

app="$PWD/Surge.app"
# iden是钥匙串里相应证书的 Common Name ,课上讲的证书的CN
iden="Mac Developer: thisiscertificatecommonname (XXXXXXXXXX)"

sign(){
    codesign --deep --force --verify --verbose -s "$iden" $1
}

find $app -name _CodeSignature | xargs -I% rm -R "%"

sign "$app/Contents/Frameworks/Sparkle.framework/Sparkle"
sign "$app/Contents/Frameworks/Sparkle.framework/Versions/A"
sign "$app/Contents/Applications/Surge Dashboard.app"

sign "$app"

0x05 套路

我看到的逆向教程,基本都是这样,到这里就结束了。上工具,脱壳。找到入口啦,跟汇编。下一步,下一步,下一步(啪啪啪键盘声)。好了,我们找到了 关键函数 ,patch,OK。

学到东西个鬼嘞!

能熟练掌握汇编的人,还需要看教程?需要看教程的人,怎么可能看得懂汇编?嗯?为啥我贴汇编?因为装逼漂亮描述程序最准确的方法还是通过汇编啦!而且伪代码这种东西,看起来和汇编一样需要技巧,宝宝其实也不看汇编的,下面努力的憋一点干货。

本次黑阔联播的主要内容有:

  • 查找字符串需要了解的常识
  • 如何正确使用神奇海螺F5 阅读反汇编源码
  • 如何鉴别反编译的结果是否正确
  • 如何识别“关键跳转”
  • 如何通过瞎猜理清函数调用关系

0x0501 字符串

上篇文章里我可能没提,不过存在一个有时可能会遇到的问题,就是中文字符串会显示为乱码。这个问题通过这个帖子3的方法调一下编码可以解决。这个似乎比OD的中文搜索引擎稍微好一点,因为设置选项多可以识别的时候也更多一点嘛。

下面的话都是我瞎猜的啊。

如果字符串被加密了的话,可以采用的思路可以比如,在虚拟机里dump内存,或者分析标准库之外被调用次数很多的函数,它可能会是加解密函数。当然libc等等库的版本要识别准,搞了半天发现目标函数是printf可就尴尬了。

还有一个小套路,看过的一篇文章,出处不可考。有的程序为了反制静态分析,会在字符串前后加一些垃圾,然后从中间引用字符串来躲避引用检查。比如,在程序里面用 const char* helloStr="fuckyouHello world";,然后调用时候用 printf(helloStr+7); 输出,如果开启了编译器优化的话,helloStr的地址就不会出现在引用中。而IDA识别字符串时候只会识别字符串开头,不会从中间搜索引用,同理,把上例的fuckyou替换成非ascii垃圾字符,逆向工具不会把它当作字符串的一部分,这时调用helloStr本身也不会被发现,从而迷惑了软件。

0x0502 慎用反编译

伪代码的用处其实是非常有限的,但是用好了也会非常节约脑力,特别是判断跳转语句特别多的代码段,IDA会比我们想象的还强大。

一般情况下,GUI程序会有很大一部分代码由IDE生成,这些代码大部分都没有用,只有视图与控制器交互的部分,即调用或引用程序内的函数有用。一般,存在输入输出的地方才会需要警惕。网上有好多奇技淫巧教我们怎么捕捉按钮事件一类的事情,找到这个一般就成功了一大半。这些代码大概扫一下汇编代码,用xref graph寻找一下目标在代码区内的跳转就可以了。

我们需要阅读的汇编源码大概就是,找到关键判断后需要更改的语句的上下文而已。对于复杂的算法,一般开发者不是用现成的库如openssl就是自己用程序实现了,而自己写代码时又几乎不可能针对产生的汇编代码进行微调,只能寄希望于编译器,而套路编译器正是IDA的强项。越用越感觉它真的很强,hopper识别不出来的if else判断,IDA几乎都能识别出来。

0x0503 鉴别代码

作为一个打死不看汇编的假司机,算是有点微小的工作。对于反汇编软件来说,他们和我们一样,也是靠猜的,只不过是一个庞大的经验集合体。首先,编译器对于类型的猜测基本都是错的。更何况有一些enum、struct、对象编译以后都会不可挽回的丢失信息,string最后就是一个个int,集合体的内容都拆成了一个个内存地址和变量。这都不算错,就像一个人说话里,语法的错误并不影响表达的意义。最心疼的是意思都不对。比如上文的 registerButtonPressed 函数,Hopper的反汇编是这样的,

void -[WindowController registerButtonPressed:](void * self, void * _cmd, void * arg2) {
    rax = [self retain];
    var_8 = rax;
    [rax refresh:__NSConcreteStackBlock];
    [var_8 release];
    return;
}

我们要的函数已经没了……所以看它反汇编对不对,看中间的变量有没有多的少的就是了。至于逻辑,反编译器就算识别不出用goto,也不会把结构搞乱的啦。

0x0504 “关键跳转”

大概两个方向。一个适用于动态调试的时候,输入或输出后下断点,如果遇到好大一坨循环,基本都是在循环存取字符串,循环过后的call一般值得注意。有的CTF题就是这样的套路。如果直接看代码的话,那也是搜索附近的函数调用了。另一个就是搜索字符串,所以字符串很重要的,上文的例子中,直接用字符串的引用就能找到包含它的函数,好多地方都是这个套路。

0x0505 调用关系

上文引用的文章1中,专门有一段介绍如何通过分析一段代码之间的调用关系来制作flowchart。在最终编译成的程序中,只有一段或者几段程序是用户的代码,剩下的存储区包括常量表,静态链接的库函数,有些弱类型的语言4还会保留运行时的函数绑定信息。在这些固定的段中寻找会快的很多。字符串也是一方面啦,不细提。

困死我了上面都是随便写写的。