[macOS翻译]MacBook 蛤壳模式的逆向工程

1,114 阅读13分钟

本文由 简悦 SimpRead转码,原文地址 alinpanaitiu.com

在连接了外接显示器的情况下关闭 MacBook 盖子会关闭并禁用内置 disp......

你刚为你的 MacBook 买了一个超宽的大显示器。接上后,你惊叹于它的像素之高。

你发现自己再也用不上 MacBook 的内置显示屏了,它总是唠唠叨叨地提醒你要把它放在下部的周边视野中。

关上机盖是不可能的,因为你仍在使用键盘和触控板,甚至时不时还会使用网络摄像头和 TouchID。因此,你需要尝试一些方法:

  • 尝试完全调低亮度,关闭显示屏。嗯,好的,但现在:
    • 你的鼠标有时会在屏幕上游荡
    • 一些窗口在那里丢失了
    • 你还在浪费 GPU 循环来渲染 600 万个未使用的像素
  • 将显示器镜像到内置屏幕。很好,这解决了前两个问题!
    • 但为什么分辨率变了 每次都要改回来吗
    • 等等,为什么我再也收不到通知了!哦。有一个设置
  • 你离开办公桌,屏幕就会进入休眠状态
  • 你回来时,屏幕亮度已经达到 6% 左右,不再完全关闭了
    • 好吧,再按一次 "亮度降低",我可以接受
    • 至少还有 "Cmd "+"Brightness Down"

为什么没有办法真正禁用这个屏幕呢?

BlackOut

因为我的 🌕 Lunar 应用程序的许多用户向我诉说了他们对无法在软件中关闭单个显示器的不满,所以我进入了显示镜像的兔子洞,自动实现了上述所有功能。

现在,有人可以使用键盘快捷键随意关闭或打开任何显示器,甚至可以自动执行上述MacBook + 显示器工作流程,在外部显示器连接或断开时触发。

但我仍然耿耿于怀的是,macOS 其实可以完全禁用内屏,但我们却只能使用这种 零亮度镜像 的可恶功能。

# 蛤壳模式

当显示器仍连接在 MacBook 上时,关闭 MacBook 盖子,内屏将从屏幕列表中消失,而外部显示器仍可用。

在笔记本电脑领域,这一功能被称为 "蛤壳模式"。恭喜你,你价值 3000 美元的一体机现在只是一个带有 USB-C 端口的 SoC。好吧,你还得到了扬声器和低效的冷却系统。

在带凹槽的 MacBook-Pro 长条形之前的时代,盖子是通过盖子上的磁铁和一些霍尔效应传感器 来检测是否合上的。因此,你只需在盖子两侧放置两块强力磁铁,就能欺骗 macOS,让它认为盖子是合上的。

在新的 2021 设计中,MacBook 配备了一个铰链传感器,不仅可以检测机盖是否关闭,还能检测关闭的角度。磁铁再也不能欺骗他们了。

但所有这些传感器可能只会在软件中触发某个事件,由处理程序决定是否禁用显示屏,并调用某个 "disableScreenInClamshellMode "函数。

那么这个函数在哪里,我们能自己调用吗?

#软件侧

从 Apple Silicon 开始,大部分用户空间代码都存放在一个名为 DYLD 共享缓存的文件中。从 Ventura 开始,该文件位于 Cryptex(只读卷)中,路径如下:

/System/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e

由于该文件主要是 macOS 框架的优化串联,我们可以使用 keith/dyld-shared-cache-extractor 提取二进制文件:

mkdir -p ~/Temp/dyld && cd ~/Temp/dyld
dyld-shared-cache-extractor /System/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e $PWD

让我们以文本格式提取已导出和未导出的符号,以便使用 ripgrep 等工具轻松搜索它们。

我使用 /usr/bin/nmfd-x 选项来利用并行化的优势。与 parallel 的语法相比,我更喜欢它的语法,因为它集成了对参数 (注意 {/} 的基名/目录名进行插值的功能。

mkdir symbols private-symbols

fd --maxdepth 1 -t f \
    . ./System/Library/*Frameworks/*.framework/Versions/A/ \
    -x sh -c 'nm --demangle --defined-only --extern-only {} > symbols/{/}'
fd --maxdepth 1 -t f \
    . ./System/Library/*Frameworks/*.framework/Versions/A/ \
    -x sh -c 'nm --demangle --defined-only {} > private-symbols/{/}'

搜索 clamshell 会得到有趣的结果。最值得注意的是 SkyLight 中的这个:

~/Temp/dyld ❯ rg -i clamshell

symbols/SkyLight
1710:00000001d44bce70 S _kSLSDisplayControlRequestClamshellState

"SkyLight.framework "负责处理 macOS 中的窗口和显示管理,它通常会导出足够多的符号供我们在 Swift 中使用,因此我倾向于采用这条路径。

让我们看看互联网上是否有我们需要的东西。我通常在 SourceGraph 上搜索代码,因为它索引了一些带有 dyld dump 的大型 macOS 仓库。不过,查找 "RequestClamshellState "会得到更有趣的东西


看来苹果开源了电源管理代码,不错!里面甚至还有最新的 ARM64 代码,我们有那么幸运吗?

下面是与我们的事业相关的内容摘录:

SLSDisplayPowerControlClient *gSLPowerClient = nil;

enum {
    kPMClamshellOpen          = 1,
    kPMClamshellClosed        = 2,
    kPMClamshellUnknown       = 3,
    kPMClamshellDoesNotExist  = 4
};

void handleSkylightCheckIn(void)
{
// ...
    // create ws power control client
    NSError *err = nil;
    gSLPowerClient = [[SLSDisplayPowerControlClient alloc] initAsyncPowerControlClient:&err notifyQueue:_getPMMainQueue() notificationType:kSLDCNotificationTypeNone notificationBlock:^(void *dict) {
        if (dict != nil) {
            handleSkylightNotification(dict);
        } else {
            ERROR_LOG("Received a nil dictionary from WindowServer callback");
        }
    }];
// ...
}

void requestClamshellState(SLSClamshellState state)
{
    /* Forward clamshell state to WindowServer
     A) a request with a clamshell state of close in interpreted as a turn off clamshell display (clamshell close)
     B) a request with a clamshell state of open in interpreted as a turn on internal and ANY external displays (clamshell open)
     */

    if (!gSLCheckIn) {
        ERROR_LOG("WindowServer has not checked in. Refusing to change clamshell display state");
        return;
    }

    NSError *err = nil;
    NSMutableDictionary *request = [[NSMutableDictionary alloc] initWithCapacity:1];
    NSNumber *ns_state = [[NSNumber alloc] initWithUnsignedChar:state];
    [request setValue:ns_state forKey:kSLSDisplayControlRequestClamshellState];
    SLSDisplayControlRequestUUID uuid = [gSLPowerClient requestStateChange:(NSDictionary *const)request error:&err];
    if ([err code] != 0) {
       ERROR_LOG("Clamshell requestStateChange returned error %{public}@", err);
    } else {
       INFO_LOG("requestClamshellState: state %u, Received uuid %llu", state, uuid);
        struct request_entry *entry = (struct request_entry *)malloc(sizeof(struct request_entry));
        entry->uuid = uuid;
        entry->valid = true;
        STAILQ_INSERT_TAIL(&gRequestUUIDs, entry, entries);
    }
    if (request) {
       [ns_state release];
       [request release];
    }
    if (err) {
        [err release];
    }
}

因此,它正在实例化一个 SLSDisplayPowerControlClient,然后调用其 requestStateChange 方法。"SLS "是与 SkyLight 相关的前缀 (可能代表 SkyLightServer),让我们看看我们的框架版本中是否有该代码。

我更倾向于使用Hopper及其Read File From DYLD Cache功能,它可以从当前正在使用的缓存中提取框架:

好了,类和方法都有了,我们来看看使用它们的是什么。由于很可能是处理电源管理的守护进程,我将在 /System/Library中查找。

看来 powerd 就是我们要找的,其中包含的代码与我们在 SourceGraph 上看到的一模一样。

❯ rg -uuu requestClamshellState /System/Library/ 2>/dev/null

/System/Library/CoreServices/powerd.bundle/powerd: binary file matches (found "\0" byte around offset 4)

❯ hopperv4 -e /System/Library/CoreServices/powerd.bundle/powerd

# 编写代码

要链接并使用 SLSDisplayPowerControlClient,我们需要一些头文件,因为 Swift 没有可用的方法签名。

在 SourceGraph 上查找 SLSDisplayPowerControlClient,会得到 比我们需要的更多

让我们创建一个桥接头,以便 Swift 可以链接到 Objective-C 符号,并创建一个 Swift 文件,在其中我们将尝试复制 powerd 的工作。

mkdir clamshell && cd clamshell
touch Bridging-Header.h Clamshell.swift
# Bridging-Header.h
#import <Foundation/Foundation.h>

@interface SLSDisplayPowerControlClient {}

- (id)initAsyncPowerControlClient:(id*)arg1 notifyQueue:(id)arg2 notificationType:(UInt8)arg3 notificationBlock:(void (^)(NSDictionary*))notificationBlock;
- (id)initPowerControlClient:(id*)arg1 notifyQueue:(id)arg2 notificationType:(UInt8)arg3 notificationBlock:(void (^)(NSDictionary*))notificationBlock;

- (unsigned long long)requestStateChange:(id)arg1 error:(id*)arg2;
@end

extern NSString* kSLSDisplayControlRequestClamshellState;
UInt8 kSLDCNotificationTypeNone = 0;
# Clamshell.swift
import Foundation

enum ClamshellState: Int {
    case open = 1
    case closed = 2
    case unknown = 3
    case doesNotExist = 4
}

var err: AnyObject?
let skyLightPowerClient = SLSDisplayPowerControlClient(powerControlClient: &err, notifyQueue: DispatchQueue.main, notificationType: kSLDCNotificationTypeNone) { dict in
    print(dict as Any)
}

func requestClamshellState(_ state: ClamshellState) {
    // Send the request
    let request: [AnyHashable: Any] = [
        kSLSDisplayControlRequestClamshellState: NSNumber(value: state.rawValue)
    ]

    var err: AnyObject?
    let uuid = skyLightPowerClient!.requestStateChange(request, error: &err)

    // Check the response
    if (err as! NSError?)?.code != 0 {
        print("Clamshell requestStateChange returned error", err?.localizedDescription ?? "")
    } else {
        print("requestClamshellState: state %u, Received uuid %llu", state, uuid)
    }
}

print(skyLightPowerClient!)
requestClamshellState(.closed)

# 编译...

要使用 swiftc 编译二进制文件,我们必须将它指向 SkyLight.framework 所在的位置,即 /System/Library/PrivateFrameworks

然后,我们告诉它使用 -framework SkyLight 链接该框架,并导入我们的桥接头。然后运行生成的二进制文件。

我更喜欢使用 entr 来运行,以观察文件的变化。左边是代码编辑器,右边是终端,我只需编辑并保存文件,然后观察右边的输出,就能更快地迭代和尝试。

swiftc \
    -F/System/Library/PrivateFrameworks \
    -framework SkyLight \
    -import-objc-header Bridging-Header.h \
    Clamshell.swift -o Clamshell
./Clamshell

# For faster iteration, watch file changes with entr:

echo Clamshell.swift Bridging-Header.h | entr -rs '\
	swiftc -F/System/Library/PrivateFrameworks \
		-framework SkyLight \
		-import-objc-header Bridging-Header.h \
		 Clamshell.swift -o Clamshell \
	&& ./Clamshell'

嗯... 没用。这个错误一点帮助都没有,网上也没有相关的信息。

<SLSDisplayPowerControlClient: 0x600000fb8720>
Clamshell requestStateChange returned error The operation couldn’t be completed. (CoreGraphicsErrorDomain error 1004.)

# 寻找错误

也许系统日志会为我们提供一些信息。可以使用 Console.app 查看,但我更喜欢在终端中通过 /usr/bin/log 工具查看。

log stream --predicate 'eventMessage contains "Clamshell"'

AMFI 关于二进制签名的一些内容。CMS 代表 加密信息语法,是 codesign 在使用证书对二进制文件进行签名时添加的内容。

kernel: (AppleMobileFileIntegrity) AMFI: '/Users/alin/Temp/dyld/clamshell/Clamshell' has no CMS blob?
kernel: (AppleMobileFileIntegrity) AMFI: '/Users/alin/Temp/dyld/clamshell/Clamshell': Unrecoverable CT signature issue, bailing out.
tccd: [com.apple.TCC:access] AUTHREQ_ATTRIBUTION: msgID=60667.1, attribution={responsible={TCCDProcess: identifier=kitty, pid=19959, auid=501, euid=501, responsible_path=/Applications/kitty.app/Contents/MacOS/kitty, binary_path=/Applications/kitty.app/Contents/MacOS/kitty}, requesting={TCCDProcess: identifier=Clamshell, pid=60667, auid=501, euid=501, binary_path=/Users/alin/Temp/dyld/clamshell/Clamshell}, },

我禁用了 GateKeeper,并从添加到安全与隐私的特殊开发者工具部分的终端运行二进制文件,因此这应该不会导致任何问题。

为了确保万无一失,我用每年 100 美元的苹果开发者证书进行了签名,从而消除了 "CMS blob "错误,但结果没有任何变化。


呼,休息一下吧

坐了很久的火车后,我刚到我和妻子正在重建的房子,想和大家分享一下这里的美景 😌。

虽然是一月份,但阳光照在脸上暖洋洋的,榛子树已经结出了黄色的柔荑花序

十年前,房子前主人的孩子们在齐膝深的雪地里行走,坐着木制雪橇滑下山,途中不慎摔伤了几棵年轻的杉树。🌲

季节在变化。


# 挖得更深

某些系统功能只有在二进制文件被苹果公司签署并拥有特定权限的情况下才能访问。通过检查 powerd 的权限,我们发现了一些令人担忧的问题。

该二进制文件似乎使用了 "com.apple.private.*"权限。这通常意味着,如果不存在所需的权限,某些 API 就会失效。

> codesign -d --entitlements - /System/Library/CoreServices/powerd.bundle/powerd

Executable=/System/Library/CoreServices/powerd.bundle/powerd
[Dict]
	...
	[Key] com.apple.private.SkyLight.displaypowercontrol
	[Value]
		[Bool] true
	...

我们可以尝试自己添加权限。我们只需创建一个 plist 文件,并在 codesign 中使用它:

# Entitlements.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>com.apple.private.SkyLight.displaypowercontrol</key>
        <true/>
    </dict>
</plist>

使用权限签署二进制文件并运行:

❯ codesign -fs $CODESIGN_CERT --entitlements Entitlements.plist Clamshell
❯ ./Clamshell
Job 1, './Clamshell' terminated by signal SIGKILL (Forced quit)

看起来我们被立即杀死了。日志流显示 AMFI 这么做是因为我们不是 Apple,我们不应该使用该权限。

kernel: mac_vnode_check_signature: /Users/alin/Temp/dyld/clamshell/Clamshell: code signature validation failed fatally: When validating /Users/alin/Temp/dyld/clamshell/Clamshell:
  Code has restricted entitlements, but the validation of its code signature failed.
Unsatisfied Entitlements: com.apple.private.SkyLight.displaypowercontrol

kernel: (AppleSystemPolicy) ASP: Security policy would not allow process: 57234, /Users/alin/Temp/dyld/clamshell/Clamshell
amfid: /Users/alin/Temp/dyld/clamshell/Clamshell not valid: Error Domain=AppleMobileFileIntegrityError Code=-413 "No matching profile found" UserInfo={NSURL=file:///Users/alin/Temp/dyld/clamshell/Clamshell, unsatisfiedEntitlements=<CFArray 0x155e1b600 [0x1f0e613a8]>{type = immutable, count = 1, values = (
    0 : <CFString 0x155e12db0 [0x1f0e613a8]>{contents = "com.apple.private.SkyLight.displaypowercontrol"}
)}, NSLocalizedDescription=No matching profile found}

# AMFI

AMFI 究竟是什么,为什么它能告诉我们在自己的设备上能做什么,不能做什么?

AMFI 是 Apple Mobile File Integrity(苹果移动文件完整性)的缩写,是在系统级执行代码签名的过程。

默认情况下,操作系统会锁定这些私有 API,因为如果我们可以使用这些 API,恶意软件或恶意行为者也可以使用它们。由于默认情况下锁定了这些应用程序接口,恶意软件作者就不会试图在重要性较低的目标上使用这些应用程序接口,因为这通常需要0-day漏洞。

归根结底,这只是另一层安全保护,如果在极少数情况下有人需要绕过它,苹果也提供了一种方法。这个过程包括禁用系统完整性保护,并添加amfi_get_out_of_my_way=1作为启动项。

# Inside a Recovery terminal (to disable SIP)

> csrutil disable
> reboot

# Inside a normal terminal after disabling SIP

> sudo nvram boot-args="amfi_get_out_of_my_way=1"
> sudo reboot now

我不建议这样做,因为这会给你带来很大风险,因为系统卷不再是只读的,代码签名也不再强制执行。

我只在短时间内做研究时保持这种状态,然后在日常使用时再打开 SIP。

如果您需要还原上述更改,请执行以下操作

# Inside a normal terminal before enabling SIP

> sudo nvram boot-args=""

# Inside a Recovery terminal (to enable SIP)

> csrutil enable
> reboot

# 不再有 AMFI?

不幸的是,即使禁用了 AMFI,我们仍然会遇到 CoreGraphicsError 1004。的确,AMFI 不再抱怨权限问题,它们被接受了,二进制文件也没有被 "SIGKILL"。

但我们仍然无法仅通过软件进入蛤壳模式。

Frida

如果你还没听说过,Frida 是一款超棒的工具,它能让你向已运行的进程注入代码,通过名称 (甚至地址) 挂钩函数,观察函数调用的方式和时间,检查参数,甚至调用自己的函数。

让我与你分享另一个我喜欢的 macOS 启动项:

sudo nvram boot-args=-arm64e_preview_abi

这个参数可以实现代码注入。现在,我们可以使用 Frida 来挂钩 SkyLight 电源控制方法,看看在关闭和打开盖子时它们是如何被调用的:

> sudo frida-trace -t SkyLight -m '-[SLSDisplayPowerControlClient *]' powerd

// Closing the lid
           /* TID 0x5427 */
  4617 ms  SLSDisplayControlRequestClamshellStateKey: 2
  4617 ms  -[SLSDisplayPowerControlClient requestStateChange:0x13cf06a60 error:0x16b8ca828]
  4628 ms     | -[SLSDisplayPowerControlClient service]
  4628 ms     | -[SLSDisplayPowerControlClient sendStateChangeRequest:0x13cf06a60 uuid:0x16b8ca7e0]
  4628 ms     |    | -[SLSDisplayPowerControlClient service]

// Opening the lid
           /* TID 0x8a17 */
 10537 ms  SLSDisplayControlRequestClamshellStateKey: 1
 10537 ms  -[SLSDisplayPowerControlClient requestStateChange:0x13cc1e1c0 error:0x16b9567a8]
 10538 ms     | -[SLSDisplayPowerControlClient service]
 10538 ms     | -[SLSDisplayPowerControlClient sendStateChangeRequest:0x13cc1e1c0 uuid:0x16b956760]
 10538 ms     |    | -[SLSDisplayPowerControlClient service]

我们至少得到了确认。在关闭盖子时,powerd 的确在调用 SLSDisplayPowerControlClient.requestStateChange(2)

让我们检查一下在 Clamshell.swift 中调用该方法时会发生什么。

我们首先在Clamshell.swift文件顶部添加一行readLine(strippingNewline: true),让二进制文件等待我们按下Enter键。这样我们就有了一个正在运行的进程,可以用 Frida 连接到该进程。

> sudo frida-trace -t SkyLight -m '-[SLSDisplayPowerControlClient *]' Clamshell

           /* TID 0x103 */
  1475 ms  SLSDisplayControlRequestClamshellStateKey: 2
  1475 ms  -[SLSDisplayPowerControlClient requestStateChange:0x600001d64510 error:0x16d8d3c90]
  1479 ms     | -[SLSDisplayPowerControlClient service]
  1479 ms     | -[SLSDisplayPowerControlClient sendStateChangeRequest:0x600001d64510 uuid:0x16d8d3a10]
  1479 ms     |    | -[SLSDisplayPowerControlClient service]

一切看起来都一样,看来我们看得不够深。

请求方法似乎要访问 service 属性,而该属性是一个 SLSXPCServiceXPC 服务 是 macOS 用于低级进程间通信的工具。

一个进程可以使用一个标签(如 com.myapp.RemoteControlService)公开一个 XPC 服务,并监听通过该服务发出的请求,其他进程可以使用相同的标签连接到该服务并发送请求。

系统会处理路由部分。还有身份验证部分。

看起来 XPC 服务也可以受限于特定的代码签名要求,这可能就是我们在这里遇到的问题吗?

让我们也用 Frida 来跟踪一下 SLSXPCService 方法:

> sudo frida-trace -t SkyLight -m '-[SLSDisplayPowerControlClient *]' -m '-[SLSXPCService *]' powerd

// Closing the lid while observing powerd
           /* TID 0x518b */
  3029 ms  -[SLSDisplayPowerControlClient requestStateChange:0x139621c60 error:0x16f0c2828]
  3029 ms  SLSDisplayControlRequestClamshellStateKey: 2
  3043 ms     | -[SLSDisplayPowerControlClient service]
  3043 ms     | -[SLSDisplayPowerControlClient sendStateChangeRequest:0x139621c60 uuid:0x16f0c27e0]
  3043 ms     |    | -[SLSDisplayPowerControlClient service]
  3043 ms     |    | -[SLSXPCService sendXPCDictionary:0x13a913be0]
  3043 ms     |    |    | -[SLSXPCService reinitConnection]
  3043 ms     |    |    |    | -[SLSXPCService enabled]
  3043 ms     |    |    |    | -[SLSXPCService enabled]
  3043 ms     |    |    |    | -[SLSXPCService connected]
  3043 ms     |    |    | -[SLSXPCService connection]
  3452 ms  -[SLSXPCService handleXPCEvent:0x13ad0ea40]
  3452 ms     | -[SLSXPCService enabled]
  3452 ms     | -[SLSXPCService cfStringToCStringPtr:0x1f3133020]
  3452 ms     | -[SLSXPCService connected]

> sudo frida-trace -t SkyLight -m '-[SLSDisplayPowerControlClient *]' -m '-[SLSXPCService *]' Clamshell

// Trying to send the clamshell request in software
  1435 ms  -[SLSDisplayPowerControlClient requestStateChange:0x6000014d4030 error:0x16b123c90]
  1435 ms  SLSDisplayControlRequestClamshellStateKey: 2
  1444 ms     | -[SLSDisplayPowerControlClient service]
  1444 ms     | -[SLSDisplayPowerControlClient sendStateChangeRequest:0x6000014d4030 uuid:0x16b123a10]
  1444 ms     |    | -[SLSDisplayPowerControlClient service]
  1444 ms     |    | -[SLSXPCService sendXPCDictionary:0x600003ec4000]
  1444 ms     |    |    | -[SLSXPCService reinitConnection]
  1444 ms     |    |    |    | -[SLSXPCService enabled]
  1444 ms     |    |    |    | -[SLSXPCService connected]
  1444 ms     |    |    |    | -[SLSXPCService autoreconnect]
  1444 ms     |    |    |    | -[SLSXPCService enabled]
Process terminated

// ...we're missing this stuff
//  3043 ms     |    |    | -[SLSXPCService connection]
//  3452 ms  -[SLSXPCService handleXPCEvent:0x13ad0ea40]
//  3452 ms     | -[SLSXPCService enabled]
//  3452 ms     | -[SLSXPCService cfStringToCStringPtr:0x1f3133020]
//  3452 ms     | -[SLSXPCService connected]

很好!还是不好?

我不知道是该为我们发现由于没有 XPC 连接而导致 Clamshell 请求无效而高兴,还是该为这意味着我们无法启用 SIP 而担心。

我想是时候深入了解一下了。

# XPC 服务

现在我们可以访问 Frida,使用方便的 xpcspy 工具嗅探 powerd 的 XPC 通信。

我在想,也许我们可以找到 XPC 监听器的端点名称,然后直接连接到它并发送原始信息,而不是依赖 SkyLight 来完成这项工作。

> sudo xpcspy --parse powerd

// Closing the lid

xpc_connection_send_message
<OS_xpc_connection: <connection: 0x13a808820> { name = (anonymous), listener = false, pid = 30630, euid = 88, egid = 88, asid = 100014 }>
<OS_xpc_dictionary> { count = 4 contents =
    "PayloadType" => <OS_xpc_uint64: <uint64: 0x81917509705f5717>: 3>
    "Command" => <OS_xpc_uint64: <uint64: 0x81917509705f572f>: 4>
    "Payload" => <data> { length = 92 bytes, contents = {
	    SLSDisplayControlRequestClamshellStateKey = 2;
	}
    "UUID" => <OS_xpc_uint64: <uint64: 0x81917509705f5647>: 41>

}

因此,我们有了 name = (anonymous), listener = false, pid = 30630

匿名监听器,还能更糟吗?PID 与 WindowServer --daemon 不谋而合,所以它肯定也是我们要发送的信息。但由于是匿名监听器,我们只能依靠 SkyLight 导出的代码来联系它。

我想我们需要回去读一些老式的汇编。


# 填充空格

在 Hopper 中重命名了一些子过程后,我们可以在图中看到 powerdClamshell 通过 SLSXPCService.reinitConnection 所走的不同代码路径。

# powerd

  1. 发现服务的 enabledconnected 属性为 true 2.
  2. 因此它退出了 reinitConnection 3.
  3. 直接通过可用的 connection 发送 XPC 字典。

# Clamshell

  • 会发现 enabledconnectedautoreconnect 都是 false
    • 所以它会以 CGError 失败。
  • 如果这些属性是 true,它就会进入右侧代码路径,即
    • 检查 0x200x28 处的属性是否为非零
    • 然后继续重新连接。

__handlers__/SLSXPCService/reinitConnection.js中添加一些Memory.readPointer调用后,我们可以看到 SkyLight 希望在0x200x28处看到什么:

OS_xpc_connectionOS_dispatch_queue_serial 属性之后有两个 NSMallocBlock

  5502 ms   -[SLSXPCService reinitConnection]
  5502 ms       arg0 obj: <SLSXPCService: 0x11f50f130>

  5502 ms       Memory.readPointer 0x8 0x101

  5502 ms       Memory.readPointer 0x10 0x11f50bb10
  5502 ms       SLSXPCService at 0x10 <OS_xpc_connection: <connection: 0x11f50bb10> { name = (anonymous), listener = false, pid = 396, euid = 88, egid = 88, asid = 100014 }>

  5502 ms       Memory.readPointer 0x18 0x11df05970
  5502 ms       SLSXPCService at 0x18 <OS_dispatch_queue_serial: Power Management main queue>

  5502 ms       Memory.readPointer 0x20 0x11f50a740
  5502 ms       SLSXPCService at 0x20 <__NSMallocBlock__: 0x11f50a740>

  5502 ms       Memory.readPointer 0x28 0x11f50a770
  5502 ms       SLSXPCService at 0x28 <__NSMallocBlock__: 0x11f50a770>

SLSXPCService.h 的内容来看,这些是 errorBlocknotificationBlock 的闭包:

@interface SLSXPCService : NSObject <SLSXPCServiceProtocol> {
	char _enabled;
	char _connected;
	char _setTarget;
	char _autoreconnect;
	NSObject*<OS_xpc_object> _connection;
	NSObject*<OS_dispatch_queue> _notifyQueue;

	/* This would be 0x20 */ id _errorBlock;
	/* This would be 0x28 */ id _notificationBlock;

	/*^block*/id _clientErrorBlock;
	/*^block*/id _clientNotificationBlock;
}

我正在一步步接近正确的代码路径,但似乎永远也走不到那一步。

下面是我在调用 requestClamshellState 之前在 Clamshell.swift 中做的事情:

guard let service = skyLightPowerClient.service else {
    print("SLSXPCService is nil")
    exit(1)
}

service.autoreconnect = true
service.errorBlock = { err in
    print("service.errorBlock", err)
}
service.notificationBlock = { notification in
    print("service.notificationBlock", notification)
}

调用 requestClamshellState 后,代码在 createNoSenderRecvPairWithQueue:errorHandler:eventHandler: 内因 SIGSEGV 而崩溃,因为它分支到了 0x0 地址。

// The crashing instruction is:
ldr    x8, [x19, #0x28]

// Because the memory at [x19, #0x28] contains 0x0

// And register x19 contains:
(lldb) po $x19
<__NSMallocBlock__: 0x600000c08330>
 signature: "v8@?0"
 invoke   : 0x1a081a958 (/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight`__75-[SLSXPCService createNoSenderRecvPairWithQueue:errorHandler:eventHandler:]_block_invoke)
 copy     : 0x1a05e0a18 (/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight`__copy_helper_block_e8_32b40r)
 dispose  : 0x1a05e09d4 (/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight`__destroy_helper_block_e8_32b40r)

# 暂时放弃

不幸的是,我在这里有点迷茫。我要休息一下,希望能像神话故事里那样,在梦中或长途跋涉中找到解决办法。

这篇文章已经比我想读的还要长了,所以如果有人读到这里,恭喜你,你有僧侣般的耐心。

如果有更好的方法来解决类似的问题,我很乐意通过联系表了解。

我并不总能高兴地得知,我在一个问题上浪费了 4 天时间,而如果有合适的工具,这个问题几个小时就能解决,但至少下次我会学会如何不在最基本的任务上写文章,让人厌烦。

发布日期

2023 年 1 月 17 日

时长

19分钟阅读,3962字

分类:

macOS逆向工程

Tags:

macbook macbook pro clamshell lid closed lid open disable screen turn off display

另请参阅:

Mac App Store 上的窗口切换器?这可能吗?

尝试突破 MacBook Pro 的 500 尼特限制(但失败了)

为什么最有用的 Mac 应用程序不在 App Store 上?