iOS软件调试初探(一个破解app的例子)

来源:互联网 发布:linux中for命令 编辑:程序博客网 时间:2024/04/28 15:03
标 题: 【原创】iOS软件调试初探(一个破解app的例子)
作 者: detecyang
时 间: 2013-07-01,09:29:51
链 接: http://bbs.pediy.com/showthread.php?t=174576

iOS软件调试初探
写在前面:
最近热衷于一款背单词软件,但是有学习限制,于是就想尝试着破解。同时也顺便学习iOS安全方面的知识。本文以某某背单词软件为例子,一步一步介绍如何调试、破解,高手请无视,有纰漏之处欢迎指正。

免责声明:
文本涉及到的内容仅供学习,切勿用于商业用途。

准备工作:
动手前在论坛学习了一下,http://www.kanxue.com/bbs/showthread.php?t=167398和http://www.kanxue.com/bbs/showthread.php?t=138472两个帖子,按照文中介绍的步骤来,但是遇到了很多其他的问题,期间问题解决的过程也很坎坷。首先介绍gdb调试的方法:在一台越狱的设备上,cydia改为开发者模式,安装

>OpenSSH,作为SSH服务端;
>GNU Debugger(gdb调试工具):在这个源中cydia.radare.org,版本为1708,低版本不支持ios4.3+。
>adv-cmds:ps命令可以查看进程信息;
>darwin cc tools:otools可以查看可执行文件的详细信息;
>Link Identity Editor:ldid签名;

>PC上安装SSH Secure Shell Client

分析程序、反汇编代码:
在本例中要做的是破解软件学单词的数量限制,在文件管理器中查看软件目录,发现Learn.db应该存储了程序的参数信息,用sqlite browser之类的软件打开数据库,看到如下:
名称:  1.jpg查看次数: 0文件大小:  13.5 KB
打开这个表,看到这一项:名称:  2.jpg查看次数: 0文件大小:  2.7 KB
应该是单词学习上限,只要修改这个值就可以轻松解除限制了,但是还看到了另一个东西:
名称:  3.jpg查看次数: 0文件大小:  3.4 KB
应该是做校验的,看来直接修改还不行,要调试代码知道如何计算这个码才可以。这里我们先直接修改nStudyLimit为50000(毫不吝啬),另外备份一下这个db,保存复制回手机中。如果此时执行程序,会提示异常数据:
名称:  4.jpg查看次数: 0文件大小:  21.5 KB

这是自然,因为擅自修改了文件,程序校验失败。用ida6.4打开软件中的执行文件,左侧可以看到程序中的函数名,根据名字猜测,看到在checkLearnNum这个函数中:
名称:  5.jpg查看次数: 0文件大小:  12.9 KB

有关键信息,应该是在这个函数里做的校验。好我们记住这个函数附近的一个地址:0007610A,一会调试代码下断点要用到。

连接SSH:
要保证pc和手机连入同一个局域网中,ssh客户端中host即为手机ip地址,端口22,第一次接入账号为root,alpine,最好修改默认密码为你自己常用的。

如果没有无线设备可供使用,可以用数据线将手机连入pc,下载itools,打开其中的SSH隧道功能:
名称:  6.jpg查看次数: 0文件大小:  16.7 KB

这时的host主机名为127.0.0.1。如图就算连接成功了:
名称:  7.jpg查看次数: 0文件大小:  14.4 KB


熟悉gdb调试:
记住以下常用命令就足够了:

ps -ax:查看当前所有进程

Gdb -p pid:附加到目标进程

Info sh:这个可以查看程序代码在内存中的偏移地址

Break:下断点

display /i $pc | $cpsr.t:显示要执行的下一句指令

continue(或c):继续执行;

Nexti(或ni):单步执行一条汇编指令

Po $rN(N为数字,打印寄存器存储的对象,寄存器实际存储的是对象的地址)

Print $rN(打印寄存器中的值)

Set $rN=xxxx(给寄存器赋值)

先在手机上启动你要调试的程序,执行ps -ax查看目标进程的pid:

588 ??         0:04.17 /var/mobile/Applications/77FFC04E-1B91-46CE-909C-CE886

然后附加到进程:

Gdb -p 588

过一会会显示........done.执行info -sh查看代码的偏移地址:

Num Basename                 Type Address         Reason | | Source    

  | |                           | |                    | | | |         

  1 LearnEnglish                - 0x8b000           exec Y Y /private/var/mobile/Applications/77FFC04E-1B91-46CE-909C-CE886A9060ED/LearnEnglish.app/LearnEnglish at 0x8b000 (offset 0x8a000)

最后的offset 0x8a000即为偏移地址,记下。此处有个窍门,info -sh后可能会输出很多信息,导致前面的都滚屏了,此时可在执行此命令后立即按pause键暂停,找到偏移地址后按方向键下,继续执行:)

此时需要先下断点,用之前我们找到的那个函数地址,执行break *(0x8a000+0x0007610A)下断点,输出以下说明成功:

Breakpoint 1 at 0x10010a

接着要再执行:

display /i $pc | $cpsr.t

这样之后我们每次单步执行都能输出下一句的代码来提示我们执行到了哪里。

此时执行c,让程序跑起来,在手机上操作程序,点击学习单词,看到gdb输出:

Breakpoint 1, 0x0010010a in -[WordsUIViewController checkLearnNum] ()

1: x/i $pc | $cpsr.t  0x10010a:  6f f0 34 ee                   blx      0x16fd74

说明断点下对了,我们找对了地方。

执行ni,向下走一步,此时要对比着ida中的代码一步步走,以防我们跟丢了:
名称:  8.jpg查看次数: 0文件大小:  21.8 KB

我们单单只看这几句:

MOVW            R2, #(:lower16:(cfstr_Nstudylimit - 0x7611C)) ; "nStudyLimit"

MOV             R1, R4

MOVT.W          R2, #(:upper16:(cfstr_Nstudylimit - 0x7611C)) ; "nStudyLimit"

ADD             R2, PC  ; "nStudyLimit"   //这里猜测应该是加载函数地址

BLX             _objc_msgSend     //然后这句是执行

MOV             R11, R0 //R0寄存器存储的应该是函数返回的值,也就是获取nStudyLimit的值。

我们ni执行到MOV R11,R0这句,执行po $r0:

(gdb) po $r0                    

50000

可以看到输出的是50000,就是我们之前修改的值。而且这里应该是NSString类型的字符串。

LDR             R1, [R0] ; "currentDevice"

LDR             R0, [R2] ; _OBJC_CLASS_$_UIDevice

BLX             _objc_msgSend

MOV            R1, #(selRef_uniqueGlobalDeviceIdentifier - 0x76164) ; selRef_uniqueGlobalDeviceIdentifier

ADD             R1, PC ; selRef_uniqueGlobalDeviceIdentifier

LDR             R1, [R1] ; "uniqueGlobalDeviceIdentifier"  //获取设备标识

BLX             _objc_msgSend

MOV             R3, R0

MOV             R0, #(selRef_stringWithFormat_ - 0x7617E) ; selRef_stringWithFormat_

MOV             R1, #(classRef_NSString - 0x76180) ; classRef_NSString

ADD             R0, PC ; selRef_stringWithFormat_

ADD             R1, PC ; classRef_NSString

MOVW           R2, #(:lower16:(stru_FC200 - 0x76198)) ; "%@%@%@"

LDR.W           R10, [R0] ; "stringWithFormat:"

MOVT.W         R2, #(:upper16:(stru_FC200 - 0x76198)) ; "%@%@%@"

LDR             R0, [R1] ; _OBJC_CLASS_$_NSString

MOV       R4, #(cfstr_1ea99222e762c6 - 0x7619A) ; "1ea99222e762c6483e165958b584009e"

ADD             R2, PC  ; "%@%@%@"     //这里应该就是将某三个值组合到一起

ADD             R4, PC  ; "1ea99222e762c6483e165958b584009e"

MOV             R1, R10

STMEA.W         SP, {R4,R11}

BLX             _objc_msgSend

MOV             R1, #(selRef_md5 - 0x761AE) ; selRef_md5

ADD             R1, PC ; selRef_md5

LDR             R1, [R1] ; "md5"            //然后算出md5值作为校验码

BLX             _objc_msgSend

MOVW            R1, #(:lower16:(selRef_isEqualToString_ - 0x761C0))

MOV             R2, R5

MOVT.W          R1, #(:upper16:(selRef_isEqualToString_ - 0x761C0))

ADD             R1, PC ; selRef_isEqualToString_

LDR             R4, [R1] ; "isEqualToString:"   //与数据库中sLimitCheckCode对比,如果两个码一致,则通过校验,继续执行。

MOV             R1, R4

BLX             _objc_msgSend

TST.W           R0, #0

BEQ             loc_76284

LDR.W           R0, [R8] ; _OBJC_CLASS_$_CallDB

MOV             R1, R6

BLX          

分析了指令之后我们继续执行,到后面再输出寄存器r0的值:

(gdb) po $r0

0d94efdXXXXfd0d2805cc99e519XXd411ea99222e762c6483e165958b584009e50000

0d94efdXXXXfd0d2805cc99e是我的设备标识(这里插一句,百度uniqueGlobalDeviceIdentifier函数是个开源方法,算法是获取手机WIFI地址,然后md5);

519XXd41是前面获取的getPersonalValue;

1ea99222e762c6483e165958b584009e是代码里的一个常量;

50000是修改的nStudyLimit。

至此,我们就知道了sLimitCheckCode的算法。继续执行知道程序算出md5:

(gdb) po $r0

9ff8f3f58883ecc51eeb32358382894e

这是根据我们当前nStudyLimit算出的校验码;

前面的:

ADD             R2, PC  ; "sLimitCheckCode"

BLX             _objc_msgSend

MOV             R5, R0

可知r5存储的是数据库中的校验码,我们只要把r5赋值为我们自己的那个校验码就能通过检查了:

(gdb) set $r5 = [[NSString] alloc] initWithString:@"9ff8f3f58883ecc51eeb32358382894e"]

这么写是为了让你看到gdb确实很强大,可以直接写代码上去,我孤陋寡闻了:)

LDR             R4, [R1] ; "isEqualToString:"

MOV             R1, R4

BLX             _objc_msgSend  //比两个字符串较

TST.W           R0, #0  //测试指令,看比较结果是否一致

BEQ             loc_76284 //不一致就跳转了

LDR.W           R0, [R8] ; _OBJC_CLASS_$_CallDB

好,看到代码按我们期望的那样继续执行了,我们直接输入continue大胆放行:)
名称:  9.jpg查看次数: 0文件大小:  21.9 KB

通过了,先别高兴的太早,我们还要做最后的步骤,将数据库中的校验码改为刚才在内存中算好的校验码:

先在程序断点的情况下执行kill结束进程,然后q退出gdb。
名称:  10.jpg查看次数: 0文件大小:  5.7 KB

复制到手机中,执行程序,可以看到启动后也没有提示数据异常了,可以正常添加生词没有限制了:)

词库中总共3万多词汇,修改的限制为5万,改一次以后就不用动了。目前还有其他问题,修改了数据库后软件中无法备份学习进度到服务器,提示数据异常,可能还有别的校验,但是这不影响正常使用,这就不再讨论了,有兴趣的同学可以继续研究。

debugserver远程调试遇到的问题及解决:
手机连接xcode真机调试一次,会向iphone中安装debugserver;cydia中安装openSSH等插件(具体看pediy论坛的iOS平台应用程序调试与分析),PC安装SSH客户端,连接手机,执行debugserver来运行要调试的程序;如果出现错误如:failed to get task of process xxxx说明app没有权限,需要在mac系统下执行codesign用Entitlements.plist重新给app签名来添加权限。如果出现签名失败的问题,是系统环境不对,需要安装Command Line Tool(最好搭建完整的越狱开发环境)。参考:http://www.cnblogs.com/Proteas/archive/2012/12/20/2826928.html

创建名为Entitlements.plist文件,内容如下:
代码:
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict>    <key>com.你要破解的软件的Bundle ID</key>    <true/>    <key>get-task-allow</key>    <true/>    <key>task_for_pid-allow</key>    <true/>    <key>run-unsigned-code</key>    <true/></dict></plist>
执行

codesign -f -s "iPhone Developer" --entitlements Entitlements.plist LearnEnglish.app

将这个app文件替换掉手机中的文件夹,记得给里面可执行文件添加执行权限(通常用iFile)。执行:

cd /Developer/usr/bin

./debugserver port:2008 /var/mobile/Applications/7F2975ED-9CB6-42E0-B2AA-A5E017E1C4BB/LearnEnglish.app/LearnEnglish

这时程序自动启动,SSH客户端也应该出现Listening port:2008,IDA中就可以远程调试了。