iOS软件调试初探(一个破解app的例子)
2013-11-27 15:29
495 查看
标 题: 【原创】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之类的软件打开数据库,看到如下:
![](http://bbs.pediy.com/attachment.php?attachmentid=80318&thumb=1&d=1372641719)
打开这个表,看到这一项:
![](http://bbs.pediy.com/attachment.php?attachmentid=80319&thumb=1&d=1372641719)
应该是单词学习上限,只要修改这个值就可以轻松解除限制了,但是还看到了另一个东西:
![](http://bbs.pediy.com/attachment.php?attachmentid=80320&thumb=1&d=1372641719)
应该是做校验的,看来直接修改还不行,要调试代码知道如何计算这个码才可以。这里我们先直接修改nStudyLimit为50000(毫不吝啬),另外备份一下这个db,保存复制回手机中。如果此时执行程序,会提示异常数据:
![](http://bbs.pediy.com/attachment.php?attachmentid=80321&thumb=1&d=1372641719)
这是自然,因为擅自修改了文件,程序校验失败。用ida6.4打开软件中的执行文件,左侧可以看到程序中的函数名,根据名字猜测,看到在checkLearnNum这个函数中:
![](http://bbs.pediy.com/attachment.php?attachmentid=80322&thumb=1&d=1372641719)
有关键信息,应该是在这个函数里做的校验。好我们记住这个函数附近的一个地址:0007610A,一会调试代码下断点要用到。
连接SSH:
要保证pc和手机连入同一个局域网中,ssh客户端中host即为手机ip地址,端口22,第一次接入账号为root,alpine,最好修改默认密码为你自己常用的。
如果没有无线设备可供使用,可以用数据线将手机连入pc,下载itools,打开其中的SSH隧道功能:
![](http://bbs.pediy.com/attachment.php?attachmentid=80323&thumb=1&d=1372641719)
这时的host主机名为127.0.0.1。如图就算连接成功了:
![](http://bbs.pediy.com/attachment.php?attachmentid=80324&thumb=1&d=1372641733)
熟悉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中的代码一步步走,以防我们跟丢了:
![](http://bbs.pediy.com/attachment.php?attachmentid=80325&thumb=1&d=1372641733)
我们单单只看这几句:
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大胆放行:)
![](http://bbs.pediy.com/attachment.php?attachmentid=80326&thumb=1&d=1372641733)
通过了,先别高兴的太早,我们还要做最后的步骤,将数据库中的校验码改为刚才在内存中算好的校验码:
先在程序断点的情况下执行kill结束进程,然后q退出gdb。
![](http://bbs.pediy.com/attachment.php?attachmentid=80327&thumb=1&d=1372641733)
复制到手机中,执行程序,可以看到启动后也没有提示数据异常了,可以正常添加生词没有限制了:)
词库中总共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文件,内容如下:
代码:
执行
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中就可以远程调试了。
作 者: 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之类的软件打开数据库,看到如下:
打开这个表,看到这一项:
应该是单词学习上限,只要修改这个值就可以轻松解除限制了,但是还看到了另一个东西:
应该是做校验的,看来直接修改还不行,要调试代码知道如何计算这个码才可以。这里我们先直接修改nStudyLimit为50000(毫不吝啬),另外备份一下这个db,保存复制回手机中。如果此时执行程序,会提示异常数据:
这是自然,因为擅自修改了文件,程序校验失败。用ida6.4打开软件中的执行文件,左侧可以看到程序中的函数名,根据名字猜测,看到在checkLearnNum这个函数中:
有关键信息,应该是在这个函数里做的校验。好我们记住这个函数附近的一个地址:0007610A,一会调试代码下断点要用到。
连接SSH:
要保证pc和手机连入同一个局域网中,ssh客户端中host即为手机ip地址,端口22,第一次接入账号为root,alpine,最好修改默认密码为你自己常用的。
如果没有无线设备可供使用,可以用数据线将手机连入pc,下载itools,打开其中的SSH隧道功能:
这时的host主机名为127.0.0.1。如图就算连接成功了:
熟悉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中的代码一步步走,以防我们跟丢了:
我们单单只看这几句:
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大胆放行:)
通过了,先别高兴的太早,我们还要做最后的步骤,将数据库中的校验码改为刚才在内存中算好的校验码:
先在程序断点的情况下执行kill结束进程,然后q退出gdb。
复制到手机中,执行程序,可以看到启动后也没有提示数据异常了,可以正常添加生词没有限制了:)
词库中总共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中就可以远程调试了。
相关文章推荐
- 【原创】iOS软件调试初探
- 【Ionic实战】一个和AngularJS的跨平台(iOS,Android) APP框架
- iOS软件"一天八杯水“app开发过程
- 标签:ios开发 界面启动 APP下载安装第一次使用一般会显示一个首次启动引导界面然后进入主界面,非首次开启APP也通常会显示一个启动界面然后进入主界面。 1、本例首次启动显示First
- Xcode4.5.1破解iOS免证书开发真机调试与ipa发布
- iOS - HuggingPriority和CompressionResistance 一个例子教你理解 UILabel 抗拉伸 抗压缩
- 工具链无效。新 App 和 App 更新必须使用公共(正式)版 Xcode 6 或更高版本以及 iOS 8 SDK 或更高版本来构建。请勿提交 Beta 版软件构建的 App。
- 【读书笔记】iOS-查看一个软件ipa包的内容
- iOS 开发 APP应用发布流程详解之真机调试过程与各种坑--2016最新版
- ios 一个app启动另一个app
- ios调试发布出去得软件版本
- ios app的真机调试与发布配置
- 软件测试(3)-基于等价类划分的一个小例子
- ios调试发布出去得软件版本
- iOS,如何模仿一个App
- 如何快速的开发一个完整的iOS直播app(原理篇)
- 如何快速的开发一个完整的iOS直播app(播放篇)
- ios学习笔记block回调的应用(一个简单的例子)
- iOS程序员利用分页和模糊查询技术实现一个App接口
- iOS 如何在一个app中调用另一个app