在前几天看到@Lakr233实现的告状专家(iOS通知转发telegram)之后,开始思考有没有必要/可能实现一个iOS上的通知滤盒。
于是就开始了奇奇怪怪的研究。首先我们知道,通知显示是在SpringBoard上的,在SpringBoard上,我们可以查看当前通知,同时也可以消除通知。于是,理论上我们可以通过CoreTrust的漏洞把我们伪装成SpringBoard的权限,从而可以读取/修改整个系统的通知。这就是我们要做的事情。
前言 经过之前的分享,相信大家已经掌握了用户级的插件开发。勤奋好学的你是否对系统级的插件也有着浓厚的性趣,本篇文章将和大家一起学习如何分析并编写一款系统级的插件。 一、目标 一步步分析并编写一个拦截系统推送的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出来:

效果很好啊!接下来只需要在jailbroken的设备上开一个frida-server,然后调试下springboard就好?不对不对,我们不是做hook的,不能走springboard获取,应该做的是逆向!于是用ipsw工具把dyld提取出来,然后再从dyld_cache里把SpringBoard相关的内容拿出来。
💡 发现直接去Simulator里拿库更方便,是分开放的,不用dyld_shared_cache那么麻烦。
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了下来:
#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
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获取通知的方法:
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
前言 经过之前的分享,相信大家已经掌握了用户级的插件开发。勤奋好学的你是否对系统级的插件也有着浓厚的性趣,本篇文章将和大家一起学习如何分析并编写一款系统级的插件。 一、目标 一步步分析并编写一个拦截系统推送的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.
BulletinBoard is a private framework which handles Local and Push notifications.
‣