其实iOS的逆向分析业界已经十分成熟了,网上也有许多有趣的尝试(一步一步实现iOS微信自动抢红包(非越狱)。本文着重于如何在非越狱机器上进行调试,出于学习及总结的目的,记录于此。
本文以破解游戏梦幻西游为例,逐步讲解整个调试流程。
->工具
完成整个过程需要准备以下几个必要工具:
1、iOSOpenDev,用于允许在Xcode上创建Dylib工程;
2、yololib,用于为二进制文件注入Dylib;
3、class-dump,用于导出 decrypted 的二进制文件的头文件;
4、CaptainHook,这是一个头文件,引入工程后,我们可以通过宏快速地 Hook 任意方法;
5、一台越狱机器,安装 cycript 及 openssh;
非必要工具:
1、dumpdecrypted,用于为AppStore包的二进制文件破壳;
注:dumpdecrypted 之所以非必要,是因为一般情况下可以从越狱市场下到已经 decrypted 后的app!如果下到的包不是 decrypted 的,请自行百度 decrypted。
->定位
首先我们要解决的问题是,在哪里注入我们的调试入口。这个定位过程必须要有越狱机器的配合。我们会通过在越狱机器(已安装应用)上运行 cycript 确定应用视图层级。
在MAC命令行运行:
->ssh root@192.168.10.11 #你的越狱手机的IP,默认越狱机器密码为 alpine
->ps -e #关掉其它应用,从进程列表中找到应用对应的进程号,一般路径带了一串MD5值的就是了
->cycript -p 2231 #进行cycript
当 cycript 运行后,就可以通过命令行运行objc代码:
cy#[[UIApplication sharedApplication].keyWindow recursiveDescription]
这里用到了私有方法 recursiveDescription ,会返回整个层级的描述。
<UIWindow: 0x15cd9de50; frame = (0 0; 667 375); autoresize = W+H; gestureRecognizers = <NSArray: 0x15e08c260>; layer = <UIWindowLayer: 0x15cd79f50>>
| <CCEAGLView: 0x15e08d2a0; frame = (0 0; 667 375); autoresize = W+H; gestureRecognizers = <NSArray: 0x15cdc2290>; layer = <CAEAGLLayer: 0x15e08c8f0>>
到这,我们定位了主View对应的类,现在我们需要写点代码Hook这个类。
->编写Dylib
通过 iOSOpenDev 创建一个Dylib工程,把主工程文件由.m 改为.mm,引入头文件 CaptainHook.h,把主工程文件.mm替换为以下内容:
#import "CaptainHook.h"
__attribute__((constructor)) static void entry() {
[[BQInjectToolHelper sharedInstance] injectToolsToMY];
}
static BQInjectToolsNavigationController *sharedMainNav = nil;
CHDeclareClass(CCEAGLView);
CHMethod(7, id, CCEAGLView, initWithFrame, CGRect, frame, pixelFormat, id, arg2, depthFormat, unsigned int, arg3, preserveBackbuffer, _Bool, arg4, sharegroup, id, arg5, multiSampling, _Bool, arg6, numberOfSamples, unsigned int, arg7)
{
//调用原来的AsyncOnAddMsg:MsgWrap:方法
id v = CHSuper(7, CCEAGLView, initWithFrame, frame, pixelFormat, arg2, depthFormat, arg3, preserveBackbuffer, arg4, sharegroup, arg5, multiSampling, arg6, numberOfSamples, arg7);
if (sharedMainNav == nil) {
BQInjectToolsMainViewController *mainVc = [[BQInjectToolsMainViewController alloc] init];
BQInjectToolsNavigationController *mainNav = [[BQInjectToolsNavigationController alloc] initWithRootViewController:mainVc];
sharedMainNav = mainNav;
}
[sharedMainNav injectToSuperView:v];
BQLog(@"BQ Inject initWithFrame:pixelFormat:depthFormat:preserveBackbuffer:sharegroup:multiSampling:numberOfSamples: success!");
return v;
}
@implementation BQInjectToolHelper
+ (BQInjectToolHelper *)sharedInstance
{
static BQInjectToolHelper *sharedInsatnce = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInsatnce = [[BQInjectToolHelper alloc] init];
});
return sharedInsatnce;
}
- (void)injectToolsToMY
{
CHLoadLateClass(CCEAGLView);
CHClassHook(0, CCEAGLView, init);
CHClassHook(1, CCEAGLView, initWithFrame);
CHClassHook(2, CCEAGLView, initWithFrame, pixelFormat);
CHClassHook(3, CCEAGLView, initWithFrame, pixelFormat, depthFormat);
CHClassHook(7, CCEAGLView, initWithFrame, pixelFormat, depthFormat, preserveBackbuffer, sharegroup, multiSampling, numberOfSamples);
BQLog(@"BQ Injected!");
}
@end
@implementation BQInjectToolsNavigationController
- (void)injectToSuperView:(UIView *)view
{
if (view == nil || self.view.superview == view) return;
[self.view removeFromSuperview];
self.view.frame = CGRectMake((view.frame.size.width - _injectToolsContentSize.width) / 2, -_injectToolsContentSize.height, _injectToolsContentSize.width, _injectToolsContentSize.height);
[view addSubview:self.view];
[self addGestureRecognizerToView:view];
}
@end
这里做了几件事:
1、申明了CCEAGLView 类,并Hook了方法 -(id)initWithFrame:(struct CGRect)arg1 pixelFormat:(id)arg2 depthFormat:(unsigned int)arg3 preserveBackbuffer:(_Bool)arg4 sharegroup:(id)arg5 multiSampling:(_Bool)arg6 numberOfSamples:(unsigned int)arg7 方法;
2、在补始化CCEAGLView 时,为该View添加子View以及Pan手势响应;
后续我们就可以通过手势,呼出我们注入的页面了。
这里有个小技巧,在cycript下,输入 [[CCEAGLView alloc] init ,并按两下 tab 键,cycript会为你联想方法,你可以把方法列表里的方法全Hook了(事实上我就是这么做的)
编写完代码后运行,这里注意保证生成的dylib的目标架构包含(armv7&arm64)。
->注入
在命令行运行:
->yololib MY.app/MY libJDYTest.dylib #注意文件的相对路径,dylib文件不能带前置目录
如果你的应用包含多个架构支持,那么yololib会分别为你的每一个架构注入dylib。运行后如下:
Reading binary: libJDYTest.dylib
2016-06-08 21:26:50.944 yololib[47766:1342053] FAT binary!
2016-06-08 21:26:50.944 yololib[47766:1342053] Injecting to arch 9
2016-06-08 21:26:50.944 yololib[47766:1342053] Patching mach_header..
2016-06-08 21:26:50.945 yololib[47766:1342053] Attaching dylib..
2016-06-08 21:26:50.945 yololib[47766:1342053] Injecting to arch 11
2016-06-08 21:26:50.945 yololib[47766:1342053] Patching mach_header..
2016-06-08 21:26:50.945 yololib[47766:1342053] Attaching dylib..
2016-06-08 21:26:50.945 yololib[47766:1342053] Injecting to arch 0
2016-06-08 21:26:50.945 yololib[47766:1342053] 64bit arch wow
2016-06-08 21:26:50.946 yololib[47766:1342053] dylib size wow 56
2016-06-08 21:26:50.946 yololib[47766:1342053] mach.ncmds 23
2016-06-08 21:26:50.946 yololib[47766:1342053] mach.ncmds 24
2016-06-08 21:26:50.946 yololib[47766:1342053] Patching mach_header..
2016-06-08 21:26:50.946 yololib[47766:1342053] Attaching dylib..
2016-06-08 21:26:50.946 yololib[47766:1342053] size 54
2016-06-08 21:26:50.946 yololib[47766:1342053] complete!
看到上面的提示就表示注入完成了。注入的地址是 @executable_path/libJDYTest.dylib ,因此最终要把dylib放到和app可执行文件相同的目录下。
->重签名
重签名需要以下三个东西:
1、libJDYTest.dylib,这个是我们已经注入到二进制的动态库;
2、配对的证书和.mobileprovision文件;
3、Entitlements.plist文件;
1我们已经生成。2的获取方式如下:
1、从 https://developer.apple.com 申请开发者证书,证书名如下iPhone Developer: YourName (YYYYYYYYY),记下证书名YYYYYYYYY;
[2]、创建一个新应用工程,在工程 Build-Setting 中,把 Code signing 的Profile和证书指定为开发者证书;
[3]、运行起新工程,在Product里点app右键,找到app的目录,在app目录下有.mobileprovision文件(第三步其实可以不必做,这么做是为了保险起见);
Entitlement.pist 是用于申明应用权限的文件,会在签名中使用到,3的获取方式如下:
1、在 https://developer.apple.com 查找到证书的TeamID;
2、按如下模板生成 Entitlement.plist;
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>application-identifier</key>
<string>XXXXXXXXXX.as.your.wish.name</string>
<key>com.apple.developer.team-identifier</key>
<string>XXXXXXXXXX</string>
<key>get-task-allow</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>XXXXXXXXXX.as.your.wish.name</string>
</array>
</dict>
</plist>
以上文件准备好后,运行如下命令:
->cp libJDYTest.dylib MY.app/
->cp embedded.mobileprovision MY.app/
->rm -rf MY.app/_CodeSignature # 如果你的应用有ext或都watch,需要对Watch和ext中的_CodeSignature同样删了,或者干脆把Watch和ext删了
->codesign -f -s YYYYYYYYY MY.app/libJDYTest.dylib
->codesign -f -s YYYYYYYYY --entitlements Entitlements.plist MY.app
->xcrun -sdk iphoneos PackageApplication -v MY.app -o $(pwd)/MYFinish.ipa
运行完后,MYFinish.ipa就是我们的最终成果,用itools装到非越狱的iphone上吧。
这里签名需要注意以下两点:
1、签名的开发者证书,在钥匙串中的 属性->信任->使用此证书时 ,必须要选择<系统默认>否则签名不成功;
2、Apple Worldwide Developer Relations CA - G2证书同上;
验证是否正确答名成功使用以下命令:
->codesign -vvvvv MYFinish.app/libJDYTest.dylib
MYFinish.app/libJDYTest.dylib: valid on disk
MYFinish.app/libJDYTest.dylib: satisfies its Designated Requirement
->调试
MYFinish.ipa 安装好后,后续重复以上步骤就可以在自己的面板上开发功能了。当然我们可以把上面的操作写成脚本,并保存为 fastPack.sh,方便快速的签名和打包,代码如下。
## 1、move yololib to /usr/local/bin/ by your self;
## 2、copy app dylib mobileprovision plist to the same path with this shell;
## 3、the all name will not support any space;
##
## ——Zhiqiang.bzq
## 2016.06.04
## Functions ##
function makeFinishName() {
# appname=$1
# name=${appname%.*}
# ext=${appname##*.}
# finishname="${name}Finish.$ext"
echo $1;
}
function checkArgument() {
if [ $# != 5 ]; then
echo "use like this : $0 appname.app libname.dylib embedded.mobileprovision entitlement.plist cername"
exit 1
fi
appname=$1
libname=$2
profile=$3
entitlement=$4
echo "checking arguments..."
if [ ! -d $appname ]; then
echo "app $appname no exist"
exit 2
fi
if [ ! -f $libname ]; then
echo "dylib $libname no exist"
exit 3
fi
if [ ! -f $profile ]; then
echo "profile $profile no exist"
exit 4
fi
if [ ! -f $entitlement ]; then
echo "entitlement $entitlement no exist"
exit 5
fi
}
function doCopy() {
echo "copying file..."
finishname=$(makeFinishName $1)
# if [ -d $finishname ]; then
# rm -rf $finishname
# fi
# cp -rf $1 $finishname
cp $2 "${finishname}/"
cp $3 "${finishname}/"
}
function doInjectAtPath() {
echo "injecting..."
finishname=$(makeFinishName $1)
name=${1%.*}
yololib "${finishname}/${name}" "$2"
}
function findExtFilesAtPath () {
ext=$1
path=$2
result=$(find $2/. -name "*.$1")
echo $result
}
function removeCodeSignAtPath () {
rm -rf "$1/_CodeSignature"
}
function doCodeSign() {
echo "code signning..."
finishname=$(makeFinishName $1)
removeCodeSignAtPath "${finishname}/_CodeSignature"
# find appex extension
appeses=$(findExtFilesAtPath "appex" "${finishname}/")
for appex in $appeses; do
removeCodeSignAtPath $appex
codesign -f -s $5 $appex
done
# for sub app
apps=$(findExtFilesAtPath "app" "${finishname}/")
for app in $apps; do
removeCodeSignAtPath $app
codesign -f -s $5 $app
done
# resign dylib
dylibs=$(findExtFilesAtPath "dylib" "${finishname}/")
for dylib in $dylibs; do
codesign -f -s $5 $dylib
done
# sign the whole app by entitlement
codesign -f -s $5 --entitlements $4 $finishname
}
function doGenerateIPA() {
echo "packing ipa..."
finishname=$(makeFinishName $1)
name=${finishname%.*}
path=$(pwd)
xcrun -sdk iphoneos PackageApplication -v $finishname -o ${path}/${name}.ipa
}
function main() {
checkArgument $*
doCopy $*
doInjectAtPath $*
doCodeSign $*
# doGenerateIPA $*
echo "done!"
}
## Script ##
main $*
但这样远远不够,至少无法快速的进行断点调试。打开我们之前为了获取.mobileprofile文件创建的工程,添加新的Target,并改为MY(和目标可执行文件名相同即可)。
在MY的Build Phases中添加Run Script,Run Script添加如下脚本:
lib_path="${BUILT_PRODUCTS_DIR}/lib${TARGET_NAME}.dylib"
local_workspace="/Users/zhiqiangbao/Desktop/HackMH"
rm -rf "${BUILT_PRODUCTS_DIR}/MY.app"
cp -rf "${local_workspace}/MY.app" "${BUILT_PRODUCTS_DIR}/"
chmod 777 "${BUILT_PRODUCTS_DIR}/MY.app/MY" #注意,这一步必须
cp -rf "${local_workspace}/embedded.mobileprovision" "${BUILT_PRODUCTS_DIR}/"
cp -rf "${local_workspace}/Entitlements.plist" "${BUILT_PRODUCTS_DIR}/"
cp -rf "${local_workspace}/fastPack.sh" "${BUILT_PRODUCTS_DIR}/"
cd $BUILT_PRODUCTS_DIR
./fastPack.sh MY.app libJDYTest.dylib embedded.mobileprovision Entitlements.plist "XXXXXXXX"
cd -
这里做了个小trick,在Xcode为新工程生成完app后,我们用自己签名过的app替换了它。所以最终被安装上手机的是被我们cracked的应用。
CMD+R运行工程试试,哈哈,大功告成。PS:游戏内的原生Log也可以看哦。
附图:
另附上开发工具dylib的git地址,别忘了改Run Script中的地址信息哦!
梦幻西游开发工具:git@gitlab.alibaba-inc.com:zhiqiang.bzq/MY_Cracked.git
用以下命令初始化:
->git submodule init
->git submodule update
如有不正之处,欢迎指正~!^ ^