在前几天看到@Lakr233实现的告状专家(iOS通知转发telegram)之后,开始思考有没有必要/可能实现一个iOS上的通知滤盒。
于是就开始了奇奇怪怪的研究。首先我们知道,通知显示是在SpringBoard上的,在SpringBoard上,我们可以查看当前通知,同时也可以消除通知。于是,理论上我们可以通过CoreTrust的漏洞把我们伪装成SpringBoard的权限,从而可以读取/修改整个系统的通知。这就是我们要做的事情。
似乎有点用的是这篇,但是不如,写个程序来调试一下?
前置分析
简单看了下,写了一个test出来:
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那么麻烦。
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 Foundation
import Dynamic
class 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
‣