在前几天看到@Lakr233实现的告状专家(iOS通知转发telegram)之后,开始思考有没有必要/可能实现一个iOS上的通知滤盒。

于是就开始了奇奇怪怪的研究。首先我们知道,通知显示是在SpringBoard上的,在SpringBoard上,我们可以查看当前通知,同时也可以消除通知。于是,理论上我们可以通过CoreTrust的漏洞把我们伪装成SpringBoard的权限,从而可以读取/修改整个系统的通知。这就是我们要做的事情。

【iOS逆向与安全】系统推送服务(APNS)拦截

前言 经过之前的分享,相信大家已经掌握了用户级的插件开发。勤奋好学的你是否对系统级的插件也有着浓厚的性趣,本篇文章将和大家一起学习如何分析并编写一款系统级的插件。 一、目标 一步步分析并编写一个拦截系统推送的deb插件 二、工具 mac系统 已越狱iOS设备:脱壳及frida调试 IDA Pro:静态分析 三、步骤 1、守护进程 守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。例如:推送服务、人脸解锁、iCloud、查找我的iPhone、iMessage等。 相应的配置目录: /Library/LaunchAgents:管理员控制特定用户的代理 /Library/LaunchDaemons:管理员提供的系统级守护进程(cydia、filza、frida等就在这) /System/Library/LaunchDaemons:iOS提供的默认守护进程 常见的进程配置文件有: 名称 描述 com.apple.apsd 推送服务 com.apple.biometrickitd.pearl 人脸解锁 c...

似乎有点用的是这篇,但是不如,写个程序来调试一下?

前置分析

简单看了下,写了一个test出来:

Swift
private func sendNotification() {        let content = UNMutableNotificationContent()        content.title = "测试通知"        content.subtitle = "Title"        content.body = "Body"        content.badge = 1        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)        let request = UNNotificationRequest(identifier:"calendar",content: content, trigger: trigger)                UNUserNotificationCenter.current().add(request) {(err) in            if let err = err {                print("Failed \(err)")            } else {                print("Success")            }        }    }

效果很好啊!接下来只需要在jailbroken的设备上开一个frida-server,然后调试下springboard就好?不对不对,我们不是做hook的,不能走springboard获取,应该做的是逆向!于是用ipsw工具把dyld提取出来,然后再从dyld_cache里把SpringBoard相关的内容拿出来。

💡 发现直接去Simulator里拿库更方便,是分开放的,不用dyld_shared_cache那么麻烦。

Bash
ipsw download ipsw --version 21A329 --device iPhone14,4  ipsw dyld extract dyld_shared_cache_arm64e SpringBoard

其实这个dyld_shared_cache还是很有意思的,apple把系统组件的绝大部分内容都放在了这里,提前加载到内存中,当App尝试import时,则可以直接把这段内存remap过去,减少了从硬盘中加载的可能性,同时也可以减少了暴露的攻击面——当尝试直接获取app时,你只能拿到一个这样的东西:

哈哈,什么都没有!然后你再去导入表里看看,是放在文件系统里的一个什么地方。但是你打开文件系统其实是看不到这个的,只有dyld_shared_cache。

在写代码的过程里看到了这个,研究了下它的做法是用UNUserNotificationCenter,通过一些方法把bundleId换掉,并且因为我们可以伪造entitlements,所以可以直接拿到其他bundleId的通知。

所以把UserNotifications.framework dump了下来:

Objective-c
#ifndef UNUserNotificationCenter_h#define UNUserNotificationCenter_h@import Foundation;#include "UNUserNotificationCenterDelegateConnectionListenerDelegate-Protocol.h"#include "UNUserNotificationServiceConnectionObserver-Protocol.h"@class NSObject, NSString;@protocol OS_dispatch_queue, UNUserNotificationCenterDelegate, UNUserNotificationCenterDelegatePrivate;@interface UNUserNotificationCenter : NSObject <UNUserNotificationServiceConnectionObserver, UNUserNotificationCenterDelegateConnectionListenerDelegate>@property (copy, nonatomic) NSString *bundleIdentifier;@property (retain, nonatomic) NSObject<OS_dispatch_queue> *queue;@property (weak, nonatomic) id <UNUserNotificationCenterDelegatePrivate> privateDelegate;@property (weak, nonatomic) id <UNUserNotificationCenterDelegate> delegate;@property (readonly, nonatomic) _Bool supportsContentExtensions;@property (readonly) unsigned long long hash;@property (readonly) Class superclass;@property (readonly, copy) NSString *description;@property (readonly, copy) NSString *debugDescription;/* class methods */+ (id)currentNotificationCenter;+ (_Bool)supportsContentExtensions;/* instance methods */- (id)init;- (id)initWithBundleIdentifier:(id)identifier;- (id)initWithBundleIdentifier:(id)identifier queue:(id)queue;- (void)setWantsNotificationResponsesDelivered;- (void)requestAuthorizationWithOptions:(unsigned long long)options completionHandler:(id /* block */)handler;- (void)requestRemoveAuthorizationWithCompletionHandler:(id /* block */)handler;- (id)notificationSettings;- (void)getNotificationSettingsWithCompletionHandler:(id /* block */)handler;- (void)setNotificationCategories:(id)categories;- (id)notificationCategories;- (void)getNotificationCategoriesWithCompletionHandler:(id /* block */)handler;- (id)pendingNotificationRequests;- (void)getPendingNotificationRequestsWithCompletionHandler:(id /* block */)handler;- (void)addNotificationRequest:(id)request;- (void)addNotificationRequest:(id)request withCompletionHandler:(id /* block */)handler;- (void)replaceContentForRequestWithIdentifier:(id)identifier replacementContent:(id)content completionHandler:(id /* block */)handler;- (void)setNotificationRequests:(id)requests;- (void)setNotificationRequests:(id)requests completionHandler:(id /* block */)handler;- (void)removePendingNotificationRequestsWithIdentifiers:(id)identifiers;- (void)removeSimilarNotificationRequests:(id)requests;- (void)removeAllPendingNotificationRequests;- (id)deliveredNotifications;- (void)getDeliveredNotificationsWithCompletionHandler:(id /* block */)handler;- (void)removeDeliveredNotificationsWithIdentifiers:(id)identifiers;- (void)removeAllDeliveredNotifications;- (id)badgeNumber;- (void)getBadgeNumberWithCompletionHandler:(id /* block */)handler;- (void)setBadgeNumber:(id)number withCompletionHandler:(id /* block */)handler;- (void)setBadgeCount:(long long)count withCompletionHandler:(id /* block */)handler;- (void)setBadgeString:(id)string withCompletionHandler:(id /* block */)handler;- (void)didReceiveNotificationResponse:(id)response withCompletionHandler:(id /* block */)handler;- (void)didChangeSettings:(id)settings;- (void)didOpenApplicationForResponse:(id)response;- (void)setNotificationTopics:(id)topics withCompletionHandler:(id /* block */)handler;- (void)getNotificationTopicsWithCompletionHandler:(id /* block */)handler;- (id)notificationTopics;- (void)getNotificationSettingsForTopicsWithCompletionHandler:(id /* block */)handler;- (id)notificationSettingsForTopics;- (id)clearedInfoForDataProviderMigration;@end#endif /* UNUserNotificationCenter_h */

这里列出了一系列方法,可以看到,有在公有api中没有暴露的initWithBundleIdentifier:(id)identifier; ,以及公有api中存在的removeDeliveredNotificationsWithIdentifiers ,以及其他的。

所以写了个demo

Swift
    func testNotificationRead(bundleId: String) {        guard NSClassFromString("UNUserNotificationCenter") != nil else {            Logger.shared.info("UNUserNotificationCenter class not found")            return        }        guard let center = (NSClassFromString("UNUserNotificationCenter") as? NSObject.Type)?            .perform(NSSelectorFromString("alloc"))?            .takeUnretainedValue() as? NSObject else {            Logger.shared.info("alloc UNUserNotificationCenter failed")            return        }        guard let center_ = center.perform(NSSelectorFromString("initWithBundleIdentifier:"), with: bundleId)?            .takeUnretainedValue() as? NSObject else {            Logger.shared.info("initWithBundleIdentifier failed")            return        }        if let bundleInCenter = center_.perform(NSSelectorFromString("bundleIdentifier"))?            .takeUnretainedValue() as? String {            Logger.shared.info("Center bundleIdentifier = \(bundleInCenter)")        }        // 在后台队列中运行通知监控循环        DispatchQueue.global(qos: .background).async {            Logger.shared.info("开始在后端子进程中监控通知")                        while true {                sleep(1)                if let delivered = center_.perform(NSSelectorFromString("deliveredNotifications"))?                    .takeUnretainedValue() as? [AnyObject] {                    Logger.shared.info("=== Delivered Notifications (\(delivered.count)) ===")                    Logger.shared.info("deliveredNotifications: \(delivered)")                } else {                    Logger.shared.info("No delivered notifications or no permission")                }            }        }                Logger.shared.info("通知监控已在后台启动")    }

现在就能获得所有通知了。

最简单的部分做完了,接下来就是封装成类,然后给他造一个ui…吗?

但是这样是循环的做法,感觉没那么优雅。于是根据下面第一篇文章,试着去Hook了一下iOS模拟器,找到了BBObserver这个类。根据在SpringBoard中的逆向结果和其他相关开发来看,我大概地总结出了SpringBoard获取通知的方法:

Swift
import Foundationimport Dynamicclass Logger {    static let shared = Logger()    func info(_ message: String) {        print("INFO: \(message)")    }    func error(_ message: String) {        print("ERROR: \(message)")    }}func loadBulletinBoardPrivateFramework() {    let frameworkPath: String    let frameworkName = "BulletinBoard"    #if targetEnvironment(simulator)    frameworkPath = "/Library/Developer/CoreSimulator/Volumes/iOS_22F77/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 18.5.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/\(frameworkName).framework/\(frameworkName)"    #else    frameworkPath = "/System/Library/PrivateFrameworks/\(frameworkName).framework/\(frameworkName)"    #endif        if dlopen(frameworkPath, RTLD_LAZY) != nil {        Logger.shared.info("成功加载 \(frameworkPath)")    } else {        Logger.shared.error("加载 \(frameworkName).framework 失败")    }}class Notification {    var bulletinID: String    var sectionID: String    var title: String    var subtitle: String    var message: String    var date: String    var origin: AnyObject    init(bulletinID: String, sectionID: String, title: String, subtitle: String, message: String, date: String, origin: AnyObject) {        self.bulletinID = bulletinID        self.sectionID = sectionID        self.title = title        self.subtitle = subtitle        self.message = message        self.date = date        self.origin = origin    }}extension Notification: CustomStringConvertible {    var description: String {        return "bulletinID: \(bulletinID), sectionID: \(sectionID), title: \(title), subtitle: \(subtitle), message: \(message), date: \(date)"    }}class BBObserverDelegate {    public static let shared = BBObserverDelegate()    private var observer: NSObject?    private var pullingHandler: ((Notification) -> Void)?    private init() {        loadBulletinBoardPrivateFramework()        guard let cls = NSClassFromString("BBObserver") else {            Logger.shared.error("BBObserver class not found")            return        }        guard let allocedObserver = (cls as? NSObject.Type)?.perform(NSSelectorFromString("alloc"))?.takeUnretainedValue() as? NSObject else {            Logger.shared.error("alloc BBObserver failed")            return        }        guard let observer = allocedObserver.perform(NSSelectorFromString("initWithQueue:"), with: DispatchQueue.main)?.takeUnretainedValue() as? NSObject else {            Logger.shared.error("initWithQueue: failed")            return        }        observer.perform(NSSelectorFromString("setDelegate:"), with: self)        self.observer = observer    }    // pass a handler    func startPullingNotificationsWithHandler(_ handler: @escaping (Notification) -> Void) {        self.pullingHandler = handler        observer?.perform(NSSelectorFromString("setObserverFeed:"), with: 0xFFFF)        observer?.perform(NSSelectorFromString("requestNoticesBulletinsForAllSections"))    }    func stopPullingNotifications() {        pullingHandler = nil        observer?.perform(NSSelectorFromString("setObserverFeed:"), with: 0)    }    @objc func observer(_ observer: AnyObject,                            addBulletin bulletin: AnyObject,                            forFeed feed: UInt64,                            playLightsAndSirens flag: Bool,                            withReply reply: @escaping (Bool) -> Void) {            if let handler = pullingHandler {                autoreleasepool {                    let info = _infoFromBulletin(bulletin)                    handler(info)                }            }            reply(true)    }    private func _infoFromBulletin(_ bulletin: AnyObject) -> Notification {        let bulletinID = bulletin.value(forKey: "bulletinID")        let sectionID  = bulletin.value(forKey: "sectionID")        let title      = bulletin.value(forKey: "title")        let subtitle   = bulletin.value(forKey: "subtitle")        let message    = bulletin.value(forKey: "message")        let date       = bulletin.value(forKey: "date")                return Notification(            bulletinID: bulletinID as? String ?? "",            sectionID: sectionID as? String ?? "",            title: title as? String ?? "",            subtitle: subtitle as? String ?? "",            message: message as? String ?? "",            date: date as? String ?? "",            origin: bulletin        )    }    func removeBulletinFromSection(_ sectionID: String, bulletin: AnyObject) {        let bulletinArray = [bulletin]        observer?.perform(NSSelectorFromString("clearBulletins:inSection:"), with: bulletinArray, with: sectionID)    }}

于是这就是我们这个业务逻辑的核心代码:找到所有通知的正确获取方式!

当然还有entitlement没有解决,但是理论上你把相关bulletinBoard的全部打开,就能正确获取运行了。

上面提到了两种方式,貌似第一种更容易被苹果接受(但是你没有entitlement一样运行不了),但是显然第二种使用observer的是苹果更喜欢的。那么接下来就是删除了。UserNotification库对删除没有限制,理论上我们已经能有所有bundleId的情况了,那么怎么调用都是对的。同样你也可以看到我实现的第二种方法是clearBulletins:inSection:,但是为什么clear才能用,delete不能用,那我就不知道了(笑)

Credits

【iOS逆向与安全】系统推送服务(APNS)拦截

前言 经过之前的分享,相信大家已经掌握了用户级的插件开发。勤奋好学的你是否对系统级的插件也有着浓厚的性趣,本篇文章将和大家一起学习如何分析并编写一款系统级的插件。 一、目标 一步步分析并编写一个拦截系统推送的deb插件 二、工具 mac系统 已越狱iOS设备:脱壳及frida调试 IDA Pro:静态分析 三、步骤 1、守护进程 守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。例如:推送服务、人脸解锁、iCloud、查找我的iPhone、iMessage等。 相应的配置目录: /Library/LaunchAgents:管理员控制特定用户的代理 /Library/LaunchDaemons:管理员提供的系统级守护进程(cydia、filza、frida等就在这) /System/Library/LaunchDaemons:iOS提供的默认守护进程 常见的进程配置文件有: 名称 描述 com.apple.apsd 推送服务 com.apple.biometrickitd.pearl 人脸解锁 c...

Parse dyld_shared_cache | ipsw

All the MANY ways you can parse DSC files.

Dev:BulletinBoard.framework

BulletinBoard is a private framework which handles Local and Push notifications.