iOS私有API检测扫描思路以及工具开发(Python3 + Django)

在这里插入图片描述

不足之处

1、class-dump有些文件会报错,由于只支持OC runtime的方式获取,可执行文件是c或者swift都无法被dump

2、私有api在公开的Framework及私有的PrivateFramework都有。

3、9.2.5的iOS系统对应的Xcode 8是有docset的,后面的Xcode都有新的文件格式了,也就是集合C的docset已经完全不同了,这个Dash作者已经和Apple的阿三工程师深入沟通,我们现在的问题是如何和阿三工程师取得联系…

导读 3.3.1大礼包

Thank you for submitting your update to xxx to the App Store.  During our review of your application we found 
it is using a private API, which is in violation of the iPhone Developer Program License Agreement section 
3.3.1; "3.3.1 Applications may only use Documented APIs in the manner prescribed by Apple and must not use or 
call any private APIs." While your application has not been rejected, it would be appropriate to resolve this 
issue in your next update.

"3.3.1 Applications may only use Documented APIs in the manner prescribed by Apple and must not use or call 
any private APIs."

The non-public API that is included in your application is terminateWithSuccess.

If you have defined a method in your source code with the same name as the above mentioned API, we suggest 
altering your method name so that it no longer collides with Apple's private API to avoid your application 
being flagged with future submissions.

Please resolve this issue in your next update to xxx.

Regards,

iPhone Developer Program

ipa包安全组扫描安全漏洞,会有一项报告是私有API警告,再早之前加上苹果的3.3.1条款被拒,近期就研究了关于私有API扫描这个主题。看了网上很多文章,基本上都是简单的介绍,大多数资料都是网易游戏开源的一个iOS private_api_ckecker项目,项目现在已经不维护了,而且是用Python2Flask写的,而且Bug好多。下面就用Python3Django重写该项目,把Bug都给修复了,而且会记录一下该扫描思路的不足以及如何识别项目中的私有API。

审核案例

1.自定义方法和私有API重名
比如说你用的Category方法和私有API重名,有种情况App没有被拒绝,但是多次提醒更新修改相关API名称,比如链接中老哥一直想用到的APIFuckApple,估计还没人敢这么用,还有种就是Category无语中用到了定义了和私有API名字一样的APIterminateWithSuccess

查询我们处理好的私有API库
SELECT * FROM all_private_apis WHERE api_name like '%terminateWithSuccess%'

在这里插入图片描述

2.使用了非公开的API

We found the following non-public API/s in your app:

allowsAnyHTTPSCertificateForHost:

If you have defined methods in your source code with the same names as the above-mentioned APIs, we suggest 
altering your method names so that they no longer collide with Apple's private APIs to avoid your application 
being flagged in future submissions.

查询我们处理好的数据库
SELECT * FROM all_private_apis WHERE api_name like '%allowsAnyHTTPSCertificateForHost%'

在这里插入图片描述

3.未执行到的私有API调用
Qzone 中曾自定义接口 _define: 但是并没有调用过,结果也被 Apple 发现并拒绝上架。
查询数据库
SELECT *FROM all_private_apis WHERE api_name = '_define:'
在这里插入图片描述

4.Tim Cook 威胁下架Uber 应用
17年某一天,纽约时报刊载了一篇Uber CEO讲玩火自焚的文章,库克威胁其下架的原因Uber的App违反了苹果的隐私协议——即使App被卸载,Uber依旧能够使用私有API获取用户设备号等信息隐秘地追踪iPhone 用户。

我靠,这也行?到底是怎么做到的呢???
在这里插入图片描述

调用方式

1.直接调用
因为私有 API 没有暴露出来,编译会报错。可以添加匿名 Category 声明下私有方法

# Category
NS_ASSUME_NONNULL_BEGIN
@interface UIView (Private)
- (id)recursiveDescription;
@end
NS_ASSUME_NONNULL_END

#import "UIView+Private.h"
@implementation UIView (Private)
- (id)recursiveDescription{
    return @"来了来了";
}
@end

# 调用
[self.view recursiveDescription];

2.字符串拼接

NSArray *parts = @[@"_df", @"ine:"];
NSString *selectorString = [parts componentsJoinedByString:@""];
[self performSelector:NSSelectorFromString(selectorString) withObject:nil];

3.导入库调用

SELECT *FROM private_framework_dump_apis WHERE source_framework = 'FTServices.framework' and class_name = 
'FTDeviceSupport'

// 私有库需要load
NSBundle *b = [NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/FTServices.framework"];
BOOL success = [b load];
    
NSLog(@"%@",@(success));
Class FTDeviceSupport = NSClassFromString(@"FTDeviceSupport");
id si = [FTDeviceSupport valueForKey:@"sharedInstance"];
    
NSLog(@"deviceColor-- %@", [si valueForKey:@"deviceColor"]);
NSLog(@"deviceName-- %@", [si valueForKey:@"deviceName"]);


// 公有库里面不需要load  CoreServices.framework
Class LSApp = NSClassFromString(@"LSApplicationWorkspace");
    
id kk = [LSApp valueForKey:@"defaultWorkspace"];
    
NSLog(@"%@",[kk valueForKey:@"allApplications"]);
NSLog(@"%ld",[[kk valueForKey:@"allApplications"] count]);

······
file:///Users/mikejing191/Library/Developer/CoreSimulator/Devices/80CC2BD5-05CB-4821-A435-270A04E3884C/data/Containers/Bundle/Application/6F4ED91A-3758-4811-B0B9-3D0E8E077783/SDKSample.app <com.tencent.wc.xin.SDKSample <installed >:0>",
    "<LSApplicationProxy: 0x7ffe60c10490> org.cocoapods.demo.JZBPay-Example file:///Users/mikejing191/Library/Developer/CoreSimulator/Devices/80CC2BD5-05CB-4821-A435-270A04E3884C/data/Containers/Bundle/Application/12122C25-FDA4-4276-8C3B-FBC35139603B/JZBPay_Example.app <org.cocoapods.demo.JZBPay-Example <installed >:0>",
    "<LSApplicationProxy: 0x7ffe60c10c90> com.whty.wxcitizencard file:///Users/mikejing191/Library/Developer/CoreSimulator/Devices/80CC2BD5-05CB-4821-A435-270A04E3884C/data/Containers/Bundle/Application/26497C5C-C7F9-4039-BE4B-31D7881BFCCB/SMT.app <com.whty.wxcitizencard <installed >:0>",
    "<LSApplicationProxy: 0x7ffe60c11020> org.cocoapods.demo.SM2-Example file:///Users/mikejing191/Library/Developer/CoreSimulator/Devices/80CC2BD5-05CB-4821-A435-270A04E3884C/data/Containers/Bundle/Application/131A8DD6-50E3-4355-ADC6-31E2B4700EA9/SM2_Example.app <org.cocoapods.demo.SM2-Example <installed >:0>",
······

检测方法

符号表
根据上面的大礼包,Apple一般会给到自查的方法,比如用nm,otool,strings,再配合grep使用
strings
检查上方重写私有API的ipa二进制文件是否包含关键词的库

strings /Users/mikejing191/Desktop/PrivateAPIDemoiPA/Payload\ 2/PrivateAPIDemo.app/PrivateAPIDemo | grep 
recursiveDescription

otool
针对目标文件的展示工具,用来发现应用中使用到了哪些系统库,调用了其中哪些方法,使用了库中哪些对象及属性

otool -L path //查看可执行程序都链接了那些库
otool -L path | grep "xxx" //筛选是否链接了xxx库
otool -D path //查看支持的架构
otool -ov path //output the Objective-C class structures and their defined methods.(输出Object-C类结构以及定义的方法) # class-dump 就是该方法的二次开发,根更友好的呈现给我们
查看该应用是否砸壳:
otool -l path | grep crypt //cryptid 0(砸壳) 1(未砸壳)

nm
显示符号表

nm path //得到Mach-O中的程序符号表
nm -nm path//目标文件的所有符号

缺点是无法检测字符串拼接方法的私有 API 调用。

动态分析
动态扫描需要应用运行起来,每当调用方法时就判断是否是私有 API,但是效率会很低,而且不能保证代码完全覆盖。

静态分析

在对二进制文件反汇编结果的基础上,进行静态分析:

找出动态调用 API 方法如 performSelector:,以及调用对象的类
检查参数,如果参数是拼接方法生成,推导求得拼接的结果

如何推导,请阅读加拿大 Laval University 发表的题为 Static Analysis of Binary Code to Isolate Malicious Behaviors 的论文。如果拼接字符串由服务端下发,依旧可以避开检查。

如果被检测到大礼包,Apple会给到具体用了哪个,可以用以下命令查看下到底用工程文件还是第三方文件

find . -type f | grep ".a" | grep -v ".app" | xargs grep advertisingIdentifier

检测思路

API分类
iOS 中的 API 大致分为三种:Published API(公开的 API)、UnPublished API(未公开的 API)和 Private API(私有 API)。 我们日常使用的 API 都是公开的 API,存放在 Frameworks 框架中。而未公开的 API 是指虽然也存放在 Frameworks 框架中,但是却没有在苹果的官方文档中有使用说明、代码介绍等记录的 API。 私有 API 则是指存放在 PrivateFrameworks 框架中的 API。通常,这两者都被称作私有 API,不过在使用方法上还是有一定区别的。苹果明确规定上架 Appstore 的应用不能使用私有 API,不过自己私下玩一玩还是挺有意思的。但是私有API你是无法查看知晓的,你就需要通过class-dump导出,网上有人已经有脚本或者已经导出到Github上了传送门,但是和我们下文的工具格式有点不匹配,因此我们自己来一条龙服务,从导出到扫描全过程分析下。
我们要把导出的文件做成和系统的一样,这样才方便和集合B统一使用
在这里插入图片描述

1、通过class-dump导出Frameworks以及PrivateFrameworks中可执行文件的头文件,通过脚本提取方法分别为SET_A集合和SET_E集合
2、通过Framework中的Header文件夹下暴露的头文件进行提取,通过脚本提取方法设置为SET_B集合
3、找到Xcode内置的com.apple.adc.documentation.iOS.docset数据库(iOS 9.3之后修改了内置数据结构,后面介绍再介绍),多表查询出对应的API,设置SET_C集合
4、那么SET_F =(SET_A - SET_B - SET_C)就是公有Framework下对应的私有API,设置为集合SET_F
5、原本B集合中的API就是私有库里面的,因此都不能被使用,则最终的私有API集合为
SET_D = SET_F + SET_E
6、使用class-dump反编译ipa包中的app文件,然后和SET_D做交集即可获取到。

以下是构建所用到的表名
集合A — framework_dump_apis framework可执行文件dump后的api集合
集合B — framework_header_apis framework暴露的头文件api集合
集合C — document_apis 内置文档docset数据集合
集合D — all_private_apis 最终私有apis集合
集合E — private_framework_dump_apis 私有framework可执行文件dump后的集合
集合F — framework_private_apis 集合A - 集合B - 集合 C剩下的apis
集合G — white_list_apis 白名单

当项目启动的时候会根据数据库不存在就会创建这7张表,其中db_names是对应的配置文件中的数组

def create_relate_tables():
    sql = ("create table %s("
           "id integer primary key AUTOINCREMENT not null, "
           "api_name varchar, "
           "class_name varchar, "
           "type varchar, "
           "header_file varchar, "
           "source_sdk varchar, "
           "source_framework varchar )")
    for db_name in db_names.keys():
        SqliteHandler().execute_sql(sql % (db_names[db_name]), ())

在这里插入图片描述

构建集合A(framework_dump_apis)

首先我们要知道如何拿到系统Framework的对应路径,在Xcode中配置启动参数DYLD_PRINT_INITIALIZERS = 1,启动之后就能在控制台拿到对应的全路径。FrameworkPrivateFramework都是在该路径下

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks

api_utils.py中我们会针对集合A调用如下

# SET_A  dump framework所有的API,Mach-o文件导出对应头文件,给framework路径作为参数
def frame_work_dump_apis(version, framework_folder):
    """
    class-dump framework下库生成的所有头文件api
    """
    # dump 目标文件的framework到指定目录 /tmp/public_headers
    framework_header_path = __class_dump_frameworks(framework_folder, 'public_headers/')

    # dump 目标文件的framework到指定目录 /tmp/public_headers/xxx.framework/Headers/xxx.h  的集合
    all_headers = __get_headers_from_path(framework_header_path)

    # 解析文件内容,获得api
    framework_apis = __get_apis_from_headers(version, all_headers)

    return framework_apis

第一步
class-dump出头文件组织结构和Xcode内置的Framework中的Headers结构一致,然后导入到工程下的/tmp/public_headers/xxxxx.framework/Headers/xxxxx.h__class_dump_frameworks里面的用到Python处理Linux命令的方法,可以参考隔壁的博客(TODO)。这里可以看到,我们不需要输出,只要成功还是失败,因此subprocess.call即可。

第二步
把所有目录下的头文件集合成数组[(frameworkname, prefix, 具体路径),()]

第三步
遍历提取头文件中的方法,类以及类型等属性[{'class':'','methods':'','type':''},{},{}],这里Python的正则提取就不介绍了,太多了,可以看工程源码,都是独立可以使用的模块
模块存在于api_helper.py,以模块PDFKit.frameworkPDFAnnotation.h为例

result =  get_apis_from_header_file('/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Devel
oper/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/PDFKit.framework/Headers/PDFAnnotation.h')
    pprint.pprint(result)

先看结果,可以对比PDFAnnotation的方法名是否全部提取

[{'class': 'PDFAnnotation',
  'methods': ['initWithBounds:forType:withProperties:',
              'drawWithBox:inContext:',
              'setValue:forAnnotationKey:',
              'setBoolean:forAnnotationKey:',
              'setRect:forAnnotationKey:',
              'valueForAnnotationKey:',
              'removeValueForAnnotationKey:'],
  'type': 'interface'}]

看下最核心的正则提取代码,如果对Python正则不熟悉的可以参考隔壁的另一个文章TODO
A:读入内存,取出头文件内容

def get_apis_from_header_file(filepath):
    with open(filepath, 'rb') as f:
        text = f.read()
        filter_text = text.decode('utf-8', 'ignore')
        # print(filter_text)
        # print('头文件读入,正在处理正则---> ' + filepath)
        apis = extract(filter_text)
        return apis
    return []
    
def extract(text):
    # print(text)
    no_comment_text = remove_comment(text)
    # print(no_comment_text)
    method = []
    method += get_objc_func(no_comment_text)
    no_class = remove_objc(no_comment_text)
    method += get_c_func(no_class)
    return method

首先这里一次性全部读入,文本decode的时候需要标注为ignore,意思是如果我读到文本有无法识别的,直接忽略,比如这个符号Copyright � 2016 Apple. All rights reserved.,会报错。
B:过滤注释

# 移除注释 //   /* */ 
def remove_comment(text):
    def handler(match):
        s = match.group(0)
        if s.startswith('/'):
            return ""
        else:
            return s

    pattern = re.compile(
        r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
        re.DOTALL | re.MULTILINE
    )
    return re.sub(pattern, handler, text)

上面使用sub替换的方式把注释替换成空串也就是删除。首先pattern编译正则表达式r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"'
通过四个或分割出四种注释,///**/\'\'""
然后匹配模式是re.DOTALL | re.MULTILINEDOTALL代表重写.匹配的规则,本身.只能匹配除了\n之外的任何字符,有了DOTALL.可以匹配任意字符。MULTILINE代表多行匹配,重写^(多行模式中匹配行首)和$(多行模式中匹配行尾巴)的行为,还有个最关键的,千万别用贪婪模式,这里本身.*默认贪婪,改为.*?后就是非贪婪模式。整句话可以理解为,一行一行为一个非贪婪模式,匹配到之后如果是注释类型,直接删除。

C:过滤@protocol xxx, xxx; 这种特殊字符串
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/EventKitUI.framework/Headers/EKCalendarChooser.h为例,这里面就有@protocol EKCalendarChooserDelegate;如果不过滤,那么匹配到@end的时候就会把所有的都包进去,明显会有问题,因此需要过滤,和上面过滤注释一样用re.sub

# 移除@protocol NSObject, OS_voucher;这种格式,避免后面的interface和protocol获取域不对
    remove_pro = re.compile("@protocol [\w ,]*;", )
    text = re.sub(remove_pro, "", text)

D:取出@interface和@protocol两块内容

interface = r"""
            @interface\s* # xx注释
            .*?
            @end
        """
 protocol = r"""
            @protocol\s* # xx注释
            .*?
            @end
        """
inter_reg = re.compile(interface, re.VERBOSE | re.MULTILINE | re.DOTALL)

这里是interface域和protocol域,编译的正则表达式也有,VERBOSE代表这个模式下正则表达式可以是多行,忽略空白字符,并可以加入注释,DOTALL代表重写.匹配的规则,本身.只能匹配除了\n之外的任何字符,有了DOTALL.可以匹配任意字符。MULTILINE代表多行匹配,重写^(多行模式中匹配行首)和$(多行模式中匹配行尾巴)的行为,没有就默认多行匹配。整句话翻译为正则匹配@interface@end的多行匹配模式,而且是.*?非贪婪遇到一组interface 和 end就要停止

# finditer查找匹配迭代器 匹配到一组或者多组@interface @end
classes = [m.group(0) for m in inter_reg.finditer(text) if m.group(0)]
#finditer是迭代器,遍历后生成classes数组。
#数组的每一项都用
class_name = re.compile("^@interface\s(\w*).*")
#提取类名

E:匹配带参或者不带参的Method ClassName,Type,组成{}返回
上一步提取完相关interface域后,把域匹配到的整一坨丢到下面的方法进行方法提取

    def _get_methods(text):
        """
        有参数
        + (void)_performExpiringActivityWithReason:(id)arg1 usingBlock:(CDUnknownBlockType)arg2;
        ['_performExpiringActivityWithReason:', 'usingBlock:']

        无参数
        + (void)_expireAllActivies;
        []

        ['_performExpiringActivityWithReason:usingBlock:',
         '_performActivityWithOptions:reason:usingBlock:',
         '_expireAllActivies',
         '_dumpExpiringActivitives',
         '_expiringActivities',
         '_expirationHandlerExecutionQueue',
         '_expiringTaskExecutionQueue',
         '_expiringAssertionManagementQueue',
         '_fireExpirationHandler',
         '_reactivate',
         '_end',
         'debugDescription',
         'dealloc',
         '_initWithActivityOptions:reason:expirationHandler:']
        :param text:
        :return: # 文件扫描结束后方法列表
        """
        # EKCalendarChooser 该字符串是有换行的
        # 有参数方法  re.DOTALL 针对换行的写法
        method = re.compile("([+-] \([ *\w]*\).*?;)\s*", re.DOTALL)
        # 去参数 提取方法
        method_args = re.compile("(\w+:)")

        # 无参数方法提取  ?!的例子如下
        #  a = re.compile(r'windows(?!7|8|9|10|xp)')
        #  x = a.match('windows10') 后缀除了上面几个,才会匹配到windows,而且不会作为值进行group捕获
        method_no_args = re.compile("[+-] \([\w *]+\)\s*(\w+)(?!:)")
        temp = []
        for m in method.finditer(text):
            if m:
                mline = m.groups()[0]
                args = re.findall(method_args, mline)
                if len(args) > 0:
                    temp.append("".join(args))
                else:
                    no_args = method_no_args.search(mline)
                    if no_args:
                        temp.append(no_args.groups()[0])

        return temp

OC的方法有三种分别是带参数,无参数,特例方法过长换行

有参数
+ (void)_performExpiringActivityWithReason:(id)arg1 usingBlock:(CDUnknownBlockType)arg2;
无参数
+ (void)_expireAllActivies;
带参数换行
- (id)initWithSelectionStyle:(EKCalendarChooserSelectionStyle)selectionStyle
                displayStyle:(EKCalendarChooserDisplayStyle)displayStyle
                  eventStore:(EKEventStore *)eventStore;

匹配方法的正则re.compile("([+-] \([ *\w]*\).*?;)\s*", re.DOTALL),末尾跟上了DOTALL导致千年的.*?变成了非贪婪模式,而是.可以匹配到任意字符包括\n,这样子他会从开始找到;即使中间被换行了也能被匹配到。,随后的finditer找到了每个方法对应的文本,根据OC的方法特性,有re.compile("(\w+:)")去匹配每个方法,如果能匹配到,说明是带参数的,把匹配到的值通过数组链接成字符串。如果没有匹配到,说明不是:结尾,就不是带参数的方法,需要re.compile("[+-] \([\w *]+\)\s*(\w+)(?!:)")正则去匹配,这个也很容易理解,前面的正则很容易理解,最后的(?!:)什么意思,首先这个()不参与捕获,?!:代表除了:之外的都能匹配上,这就正好剔除了多参数,直接捕获前面的([\w+])的方法名字即可。同理,下面的protocol包裹的模块也一样提取。
最后提取后的方法就是这种格式
methods.append({"class": cn, "methods": temp, "type": "interface"})
F:删除interface和protocol两大块
上面用过的interface和protocol需要和注释一样用sub方法替换成空串移除

def remove_objc(text):
    p = r"""
            @interface\s*
            .*?
            @end
            |
            @protocol\s*
            .*?
            @end
    """
    pattern = re.compile(p, re.VERBOSE | re.MULTILINE | re.DOTALL)
    return re.sub(pattern, "", text)

G:匹配c方法 {‘type’: ‘C/C++’, ‘ctype’: [‘Coretext’]}
下面剩下匹配的C方法,这个就不介绍了,直接去源码看,可能匹配到的值会有一点误差,总体上和上面的匹配方式类似。

第四步
把上述信息组装成对应的keyvalue,对应framework_dump_apis表中的字段

# 从头文件正则出来进行模型组装
def __get_apis_from_headers(sdk_version, all_headers):
    # 路径遍历(frameworkname, prefix, 具体路径)
    framework_apis = []
    for header in all_headers:
        # [{'class':'','methods':'','type':''},{},{}]
        apis = api_helpers.get_apis_from_header_file(header[2])
		# 根据上面返回的数据结构进行组装,把数组拆成一条条数据
        for api in apis:
            class_name = api["class"] if api["class"] != "ctype" else header[1][0:-2]
            method_list = api["methods"]
            m_type = api["type"]
            for m in method_list:
                tmp_api = {}
                tmp_api['api_name'] = m
                tmp_api['class_name'] = class_name
                tmp_api['type'] = m_type
                tmp_api['header_file'] = header[1]
                tmp_api['source_sdk'] = sdk_version
                tmp_api['source_framework'] = header[0]
                framework_apis.append(tmp_api)
    return framework_apis

第五步
多插入库 右侧数据结构[{'class':'','methods':'','type':''},{},{}]

# (:api_name,:api_name,:api_name,:api_name,:api_name,:api_name)
# 多插
def insert_apis(table_name, datas):
    """
    Mysql
    https://dev.mysql.com/doc/connector-python/en/connector-python-api-mysqlcursor-executemany.html
    如果是 [(),(),()] 则用%s
    如果是[{},{},{}] 就用 :name取值
    """
    sql = "insert into " + table_name + " (api_name,class_name,type,header_file,source_sdk,source_framework) values (:api_name,:class_name,:type,:header_file,:source_sdk,:source_framework)"
    return SqliteHandler().insert_many(sql, datas)

此时,公有库Dump出来的所有API表就建立好了,可以查看framework_dump_apis表,里面根据关键字能搜索到你平时用的API,一共有142724
注意:
当我们在Framework目录下进行dump的时候有些结果是嵌套在里面的,比如Framework内部还有Framework,比如AVFoundation.Frameworks,提取的时候千万不能忘掉,而且每个版本有可能不同,需要注意

在这里插入图片描述
那么最后提取出来是142724

构建集合B(framework_header_apis)

Framework的Header中头文件的路径获取方式已经介绍过了

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks

由于我们构建集合A的时候Dump出来的头文件结构规则和Framework结构一致,所以和构建集合A不同的是就是不需要dump,直接把头文件导出,然后挨个分析导出对应的数据结构即可,然后统一多插入库即可,代码和第一步用到的一样。
注意:
这里的结构和Framework可执行文件那里一样会出现嵌套结构,虽然我们自己dump到tmp目录下是不会有,但是公用代码的话,这里也需要处理一下上面嵌套的结构,可以在上面给的路径下看到对应的AVFoundation.Framework也一样嵌套

可以看下简单的日志路径:

头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Headers/AVAudioUnitReverb.h
头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Headers/AVPlayerMediaSelectionCriteria.h
头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Headers/AVDepthData.h
头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Headers/AVCaptureFileOutput.h
头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Headers/AVAudioUnitTimeEffect.h
头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Headers/AVUtilities.h
头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Frameworks/AVFAudio.framework/Headers/AVAudioUnitSampler.h
头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Frameworks/AVFAudio.framework/Headers/AVAudioEngine.h
头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Frameworks/AVFAudio.framework/Headers/AVAudioUnitGenerator.h
头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Frameworks/AVFAudio.framework/Headers/AVAudioTime.h
头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Frameworks/AVFAudio.framework/Headers/AVAudioUnitMIDIInstrument.h
头文件读入,正在处理正则---> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AVFoundation.framework/Frameworks/AVFAudio.framework/Headers/AVAudioUnitEffect.h

A集合和B集合主要的提取逻辑都是在api_utils.py文件中,已经加上注释。
那么最后提取出21551

构建集合C(document_apis)

Xcode 9以下,Apple的文档是以docSet的格式存在的。这是官方提供的XML信息,里面包含了所有版本的文档信息。

# 各版本 iOS docSet 的元信息
https://developer.apple.com/library/downloads/docset-index.dvtdownloadableindex
# iOS 8.1 docSet
https://devimages-cdn.apple.com/docsets/20141020/031-07735-A.dmg
# iOS 9.3.5 docSet
https://devimages-cdn.apple.com/docsets/20160321/031-52212-A.dmg

iOS 9.3.5是最后一个能获取到的docset文件了。下载后的com.apple.adc.documentation.iOS.docset文件,显示包内容打开docSet 内部的 Contents/Resources/docSet.dsidx就是我们要获取到的集合C,把这个文件拖进Navicat,看下表结构
在这里插入图片描述
打开我们的ZTOKEN表,表字段ZTOKENTYPE就是我们要关注五种方法类型,主要是以下几个

  • func(pk=1) 全局C函数
  • instm(pk=4) instance method 对象方法
  • clm (pk=2) class method 类方法
  • intfm (pk=6) interface method (- 协议)
  • intfcm (pk=22)interface class method (+ 协议)
def get_dsidx_apis(db_path):
    
    sql = "SELECT T.Z_PK," \
          " T.ZTOKENNAME," \
          " T.ZTOKENTYPE," \
          " T.ZCONTAINER, " \
          "F.ZDECLAREDIN FROM ZTOKEN as T" \
          " INNER JOIN ZTOKENMETAINFORMATION as F ON T.Z_PK=F.ZTOKEN" \
          " WHERE ZTOKENTYPE IN (1,2,4,6,22)"
    return SqliteHandler(db_path=db_path).execute_select(sql,())

打开ZTOKENPK=77为例,ZTOKENNAMEremoveFromRunLoop:forMode:,ZTOKENTYPE对应的4,查找ZTOKENTYPE表找到的是instmZCONTAINER=48,查找ZCONTAINER表,找到的值是CADisplayLink,通过连表查询ZTOKENMETAINFORMATIONZTOKEN,查找ZTOKENMETAINFORMATION值为77的表数据,ZDECLAREDIN外链ZHEADER,里面的ZHEADERPATHFRAMEWORKNAME分别代表头文件和framework

db_path是我们下载的docset文件的路径,首先通过查询ZTOKENZTOKENMETAINFORMATION进行连表查询,然后再根据对应的字段查对应的表把我们建的数据库表字段对应好,然后组装成能进行多插的数据结构,插入对应的表即可,一共32150

api_name     ZTOKEN--- ZTOKENNAME字段
class_name   ZTOKEN--- ZCONTAINER-- > ZCONTAINERNAME字段
type         ZTOKEN--- ZTOKENTYPE字段
header_file  ZTOKEN--- ZTOKENMETAINFORMATION--- ZDECLAREDIN字段---ZHEADER--> ZHEADERPATH字段
source_sdk   12.1
source_framework  ZTOKEN--- ZTOKENMETAINFORMATION--- ZDECLAREDIN字段---ZHEADER--> FRAMEWORKNAME字段

虽然说iOS 9之后咱们能用到的API基本没太大的变化,也能用上面的方式进行提取,但是如果要精益求精,就必须按新的API数据结构来提取了,具体如下

但是在iOS 9.3.5之后,Xcode不在内置docset数据库,而是换了一种数据结构,反正看起来虽然有点逻辑,但是很难提取完整。

但是Dash却能把新版本文档呈现出来,而且附上了一句话release
早期发布更新提醒

Dash won’t automatically support the iOS 10, macOS 10.12, watchOS 3
and tvOS 10 docs. I’m working on a version of Dash that supports the
new docs and will release an update as soon as possible."

后期在Apple的帮助下成功读懂了新文档格式

“The Apple API Reference docset now reads the docs from within Xcode
8. This reduces disk space usage while also allowing me to modify & improve the docs at display-time. Thanks a lot to the Xcode team at
Apple for helping me understand the new documentation format!”

咱自己也试了,咱现在的问题如何联系Apple的阿三工程师帮我们理解下最新版本的文档格式
在这里插入图片描述


Xcode 9之后的API 内置在一个Framework里面,主要是两个文件:map.db和cache.db

/Applications/Xcode.app/Contents/SharedFrameworks/DNTDocumentationSupport.framework/Versions/A/Resources/external

1、以UIButton为例,在map.db里面查询对应的uuid

select uuid from map where source_language = 1 and reference_path = 'uikit/uibutton'

2、然后到cache.db的refs表中查询到对应的data_id

select data_id from refs where uuid = 'hcOyO61dSB'

3、上面根据uuid拿到的data_id是2187,然后在同级目录下找到fs文件夹,找到对应的资源文件


上面拿到的文件是经过苹果最新的无损压缩算法LZFSE进行压缩的,Github上已经有人实现了LZFSE算法实现,下载后编译得到lzfse,然后放进/usr/local/bin

lzfse -decode -i /Applications/Xcode.app/Contents/SharedFrameworks/DNTDocumentationSupport.framework/Versions/A/Resources/external/fs/2187 -o /Users/mikejing191/Desktop/2187.json

解压后的文件是一个字符串,也不是Json字符串,感觉他是由许多个Json字符串组合而成,你可以通过以下简单的算法拿到一段段的Json,但是有时候解析出来也不是正确的Json格式,就非常恶心了
但是这个文件根据里面的线索,应该就是Apple现在最新的文档呈现格式
在这里插入图片描述
而且里面有这种我们根据之前数据库找到的相关信息

"s":[{"k":"instm","t":{"x":"initWithFrame:collectionViewLayout:"}
def get_decode_json(filepath):
    with open(filepath, 'rb') as f:
        text = f.read()
        filter_text = text.decode('utf-8', 'ignore')
        # print(filter_text)
        return filter_text
    return []
# 2439 是UIView
if __name__ == '__main__':
    result = get_decode_json('/Users/mikejing191/Desktop/2035.decode.json')

    num = 0
    result_array = result.split('}{')

    result = ''

    for str in result_array:
        print('')
        if num == 0:
            js = str + '}'
        elif num == len(result_array)-1:
            js = '{' + str
        else:
            js = '{' + str + '}'
        num += 1
        print(js)

但问题是,即使这样拆开了拿,也不一定拿到的每个字符串就是Json字符串了,而且他的key都是基本上一个字母,不知道具体的含义,很难精准的提取出需要的API,如果有朋友能提取到数据库,可以把提取到的数据库分享下,非常感谢

构建集合E(private_framework_dump_apis)

由于集合A已经把结构调整好了,代码都一样,因此只要把集合A里面的路径改成如下即可

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks

一共547128

构建集合F (framework_private_apis)

提取集合A中所有api数据集合SET_A,并且创建一个空的私有API集合PR_API
1、遍历SET_A,判断api是否以_开头,如果是加入到PR_API集合中
2、其他API既不在集合B(头文件API集合),也不在集合C(docset API集合),那么也加入到PR_API集合中
3、不在集合B/集合C的判断条件是 Sql语句中的Where语句,条件是api_name,class_name,source_sdk


def api_is_exist_in_table(table_name, api_obj):
    sql = "SELECT * FROM %s WHERE api_name = ? and class_name = ? and source_sdk = ?;" % table_name
    parameters = (api_obj['api_name'], api_obj['class_name'], api_obj['source_sdk'])
    return SqliteHandler().execute_select_one(sql, params=parameters)
# 所有公有framework下的API计算如下:
    # 属于公有11707
    # 属于私有102266
    # 属于私有下划线28751
    # 去重前 - --公有库内的私有API length:131017

这里从公有库中提取去来的API需要根据class_nameapi_name进行去重,Python库itertools提供了groupby方法,专门给数组根据key进行分组,以下是groupby的例子:

案例如下,根据date排序,分组之后取出该组下第一个即可,那么上面是根据类名和方法名分组,去重取出第一个即可
rows = [
    {'address': '5412 N CLARK', 'date': '07/01/2012'},
    {'address': '5148 N CLARK', 'date': '07/04/2012'},
    {'address': '5800 E 58TH', 'date': '07/02/2012'},
    {'address': '2122 N CLARK', 'date': '07/03/2012'},
    {'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'},
    {'address': '1060 W ADDISON', 'date': '07/02/2012'},
    {'address': '4801 N BROADWAY', 'date': '07/01/2012'},
    {'address': '1039 W GRANVILLE', 'date': '07/04/2012'},
]
def group_by_date(obj):
    return obj['date']
x = sorted(rows, key=group_by_date)

# 打印如下
[{'address': '5412 N CLARK', 'date': '07/01/2012'},
 {'address': '4801 N BROADWAY', 'date': '07/01/2012'},
 {'address': '5800 E 58TH', 'date': '07/02/2012'},
 {'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'},
 {'address': '1060 W ADDISON', 'date': '07/02/2012'},
 {'address': '2122 N CLARK', 'date': '07/03/2012'},
 {'address': '5148 N CLARK', 'date': '07/04/2012'},
 {'address': '1039 W GRANVILLE', 'date': '07/04/2012'}]

y = groupby(x, group_by_date)

for g, l in y:
    print(g)
    print(list(l)

# 打印如下
07 / 01 / 2012
    [{'address': '5412 N CLARK', 'date': '07/01/2012'}
    {'address': '4801 N BROADWAY', 'date': '07/01/2012'}]
07 / 02 / 2012
    [{'address': '5800 E 58TH', 'date': '07/02/2012'},
     {'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'},
     {'address': '1060 W ADDISON', 'date': '07/02/2012'}]
07 / 03 / 2012
    [{'address': '2122 N CLARK', 'date': '07/03/2012'}]
07 / 04 / 2012
    [{'address': '5148 N CLARK', 'date': '07/04/2012'},
     {'address': '1039 W GRANVILLE', 'date': '07/04/2012'}]

以下根据API的class_name和api_name进行去重

# api去重 根据api_name和class_name
def deduplication_api_list(apis):
    """
    相同类名和相同方法名去重
    :param apis:
    :return:
    """

    def group_by_api(api):
        return api['api_name'] + '/' + api['class_name']

    new_apis = []

    # 先排序
    apis = sorted(apis, key=group_by_api)

    # 再根据类名和方法名成组
    for group, itr in groupby(apis, key=group_by_api):
        l = list(itr)
        if l and len(l) > 0:
            new_apis.append(l[0])

    return new_apis

去重后公有库内的私有API还剩 128854

构建最终集合D (all_private_apis)

最终的私有API集合只要把集合F和集合E合并入库即可,一共675982

构建完Log

******************
SET_A
142724
********************
********************
SET_B
21551
*******************
********************
SET_C
32150
********************
********************
SET_E
547128
********************
********************
所有公有framework下的API计算如下:
属于公有11707
属于私有102266
属于私有下划线28751
********************
去重前---公有库内的私有API length:131017
start group by....
去重后----公有库内的私有API length:128854
公有库下的私有API插入最终集合---all_private_apis---128854
私有库API集合取出插入最终集合---all_private_apis---547128
SET_D  
675982
********************
SET_F
公有库下的私有API插入独立集合F集合---framework_private_apis---128854
********************

构建完的数据库mkj_private_apis.db,云盘地址(链接: https://pan.baidu.com/s/1bsMEp-Xs4LVr7TB3cfcfew 提取码: zkcu 复制这段内容后打开百度网盘手机App,操作更方便哦),不想自己构建的可以下载下来放进根目录

扫描私有API

在这里插入图片描述

1.解压ipa,提取Mach-O

1、用zipfile解压项目到tmp目录下
2、首先得安装macholib库,通过python -mmacholib find xxxxxx扫描路径下的可执行文件,默认扫描到的是数组,提取出第一个就是项目可执行文件。

python3 -mmacholib find /Users/mikejing191/Desktop/ipa1/Payload/PrivateAPIDemo.app

3、strings去获取可执行文件下可打印字符。strings主要是获取非文本文件中包含的文本内容,用\n去分割成集合用set类型去表示集合1

/usr/bin/strings /Users/mikejing191/Desktop/ipa1/Payload/PrivateAPIDemo.app/PrivateAPIDemo
WifiManagerHandler
ViewController
Private
AppDelegate
UIApplicationDelegate
recursiveDescription
......

4、otool -L 提取项目中用到的依赖库PublicFramework和PrivateFramework

FVFXGM44HV29:~ mikejing191$ otool -L /Users/mikejing191/Desktop/ipa1/Payload/PrivateAPIDemo.app/PrivateAPIDemo 
/Users/mikejing191/Desktop/ipa1/Payload/PrivateAPIDemo.app/PrivateAPIDemo:
	/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1575.17.0)
	/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.250.1)
	/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 1575.17.0)
	/System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 61000.0.0)

5、class_dump从Mach-O文件中导出头文件信息,解析出类名,协议和变量名一样用set类型集合2

def get_app_available(dump_result, pid):
    """
    处理dump出来的 property  protocol以及class
    interface 类名
    protocol 协议名
    private m文件私有属性
    prop    property属性
    """
    interface = re.compile("^@interface (\w*).*")
    protocol = re.compile("@protocol (\w*)")
    private = re.compile("^\s*[\w <>]* [*]?(\w*)[\[\]\d]*;")  # m文件私有的变量
    prop = re.compile("@property\([\w, ]*\) (?:\w+ )*[*]?(\w+); // @synthesize \w*(?:=([\w]*))?;")  # 属性
    ......

该函数会把ipa可执行文件中的dump出来的interface,protocol,private和私有property进行提取作为集合2,如果不是特别敏感的话,其实这个集合2你可以理解为空集合
6、集合1 - 集合2,由于是set类型,可以通过减法进行过滤得到集合3。由于上面集合2可以做空,那么集合3就等于集合1
7、class-dump的分析结果通过正则匹配到方法,这里dump出来的结果和上面做集合1的时候dump的framework可执行文件中头文件的格式是一样的,直接用api_helpers.py类里面的方法提取api集合Method集合4

app_apis = []
    for m in app_methods:
        class_name = m['class'] if m['class'] != 'ctype' else 'cur_app'
        method_lists = m['methods']
        m_type = m['type']
        for m in method_lists:
            tmp_api = {}
            tmp_api['api_name'] = m
            tmp_api['class_name'] = class_name
            tmp_api['type'] = m_type
            app_apis.append(tmp_api)

提取后组装成我们需要的格式
8、步骤4拿到的PublicFramework作为sql语句的条件查询SET_D(all_private_apis表),如果有白名单的话,再把白名单的私有API过滤,得到最终该扫描项目用到的框架里面的私有API集合5

# 从SET_D 私有API库里面查找api_name 而且framework不属于参数,而且不在白名单里面
def get_private_api_list(framework=None):
    framework_str = _get_sql_in_strings(framework) # in frameworks
    private_db_name = db_names["SET_D"]
    white_list_containers = _get_white_lists_results()
    # 有frame过滤条件s
    if framework_str:
        sql = "select * from %s group by api_name, class_name having source_framework in "%(private_db_name) + framework_str + " and api_name not in " + white_list_containers + ";"
        params = ()
    else:
        sql = "select * from %s group by api_name, class_name having api_name not in "%(private_db_name) + white_list_containers + ";"
        params = ()
    private_apis = SqliteHandler().execute_select(sql, params)
    print(sql)
    return private_apis

SELECT * FROM all_private_apis GROUP BY api_name, class_name HAVING source_framework in 
('CoreFoundation.framework', 'UIKit.framework', 'Foundation.framework', 'UIKitCore.framework')

这里私有API库中没有UIKit,而是用UIKitCore来代替,因此这个需要在匹配到UIKit的时候加上对应的UIKItCore.framework
9、集合3 和 集合5取交集,判断集合5中的api_name是否在集合3里面,把在的重新生成一个集合6,这里集合3可以理解为剩余字符串的API关键字,如果和集合5私有API集合有交集,那么就暂且认为是有可能出现的私有API,统一为集合6,理论上这已经是最后一步,能拿到所有匹配到的数据了,为了更精确才有最后第十步
10、遍历集合6,和集合4产生交集,由于4和6都是有api_name,class_name等详细信息的集合,因此最终根据这两个值为Key产生的交集,才算的上真正的私有API调用,存在就是私有API,不存在就不是私有API。

扫描结果举例

**************************************************
App可见Strings : 14634
App协议,变量属性 : 3954
剩下的字符串--->  String -  App协议,变量属性  : 11640
App方法名app_methods: 4088
App用的Public对应的private apis length :15125
**************************************************
strings剩余可见字符串关键字和Publick对应的私有API集合交集后的私有API--->347
**************************************************
最终API扫描结果
method_in_app:0
method_not_in_app:347
private framework:0
**************************************************

使用方式和流程

上面介绍了如何构建私有API库,还有一些不足,搜集可能不全面。还介绍了用入库的私有API如何进行ipa扫描,下面介绍下如何用Python3 + Django环境去使用。

1.构建私有API数据库

如果用现成的跑完的数据库,可以从这里下载,云盘地址(链接: https://pan.baidu.com/s/1bsMEp-Xs4LVr7TB3cfcfew 提取码: zkcu 复制这段内容后打开百度网盘手机App,操作更方便哦),这里面的表名对应的用途上面有介绍。

如果你会自己编译入库,就在config.py文件中找到sdks_configs,配置对应的路径地址,framework_pathprivate_framework_path对应的是Framework可执行文件的路径,framework_header_path头文件路径,前者需要自己dump,后者可以直接用,具体怎么找路径可以在上面构建集合A找到,docset_path路径可以自己下载setdoc文件,上面构建集合C也是下载下来找到db文件的路径,setdoc文件Apple给的最后一份,可以自己下下来,试着跑脚本入库。

最终会在项目根目录多一个数据库文件,用于扫描
在这里插入图片描述

2.虚拟环境配置

virtualenv方式

1.进入项目文件夹,用virtualenv创建虚拟环境,没有该工具用pip install virtualenv / pip3 install virtualenv 安装

2.virtualenv venv

3.virtualenv -p /usr/local/bin/python3 venv # 创建3的环境

4.pip install -r requirements.txt # 虚拟环境导入依赖

5.. venv/bin/activate # 启动虚拟环境

Pycharm方式

1.下载项目下来,用Pycharm打开,然后点击Pycharm — Preference — Project — Project Interpreter配置虚拟环境

2.点击右边的齿轮,选择add,Virtualenv Environment — New Environment 默认确定即可

3.打开Pycharm下面的Terminal,进入虚拟环境,安装依赖包

4.安装 pip install -r requirements.txt

5.然后build_apis_db.py文件可以单独跑,就会在项目主目录下生成一个tmp文件夹生成对应frameworkdump之后的头文件

6.最后自动会正则这些头文件,然后写入mkj_private_apis.db对应的表中进行后续匹配

3.直接脚本运行

把下载好的db文件或者自己编译好的db文件如上面所示出现在根目录,然后打开check_private_apis.py文件,修改main函数里面的chech_multi(path)的参数,对应的path就是ipa文件所在目录,脚本会批量扫描目录下所有ipa并输出Excel,可以在项目tmp目录中找到生成的Excel文件
在这里插入图片描述
当然,这里的私有API都是举例测试用的,这里的各种信息是扫描ipa包里面的plist文件和mobileprovision文件出来的检查ipa信息工具,下面的私有API就是根据我们抛出来的数据库比对出来的,正常情况下是无信息的,需要再完善下,如果扫到了也需要人工干预确认下。

4.Django本地环境运行

Django不熟悉的可以看看另一个文章虚拟环境启动Django的Hello World
上面已经安装配置好了Django的运行环境,安装好了所有依赖,依然cd到项目根目录,然后执行启动虚拟环境

. venv/bin/activate
python private_apis_app/manage.py runserver

启动信息如下
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

July 10, 2019 - 11:14:01
Django version 2.2.3, using settings 'private_apis_app.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

这里Django会有红色的警告,告诉你数据库没迁移,不过我们先用不到,可以无视他,然后打开http://127.0.0.1:8000/check/,直接把ipa包拖入页面区域,然后等跑数据,最终也会出现在页面上
在这里插入图片描述

项目地址:
Python3私有API扫描工具
ipa信息扫描工具

参考文章:
iOS 私有API扫描总结
iOS 私有 API 调用检测机制探讨
iOS 私有API获取
Docsets问题
应用安全审计
How do I check where my app is using IDFA
私有API-iOS10 openURL方法跳转到设置界面失效的解决方法
privates好房的大佬总结
Django和Flask入门
Django备忘录
静态扫描Git库
RuntimeBrowser库,所有API集合
xlswriter
python -m mod
utf-8 can’t decode byte…的解决方法
mysql excutemany
otool 用途

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页