fruity: Support networked lockdown for CoreDevice

Where we need to provide the remote unlock host key as part of the
RSDCheckin.

Kudos to @as0ler and @mrmacete for reporting and helping get to the
bottom of this one.
This commit is contained in:
Ole André Vadla Ravnås committed 2025-01-23 18:41:01 +01:00
1 parent af9c36ce6e
commit d1df7922bc
3 files changed
+189 -18

No files matched your search

+18 -2
View File
@@ -129,6 +129,7 @@ namespace Frida.Fruity {
private ConnectionType _connection_type;
private string _udid;
private string _name;
private Bytes? _remote_unlock_host_key;
private Promise<Tunnel>? tunnel_request;
@@ -153,6 +154,13 @@ namespace Frida.Fruity {
var reader = new XpcObjectReader (device_info);
_name = reader.read_member ("name").get_string_value ();
reader.end_member ();
if (reader.has_member ("remoteUnlockHostKey")) {
_remote_unlock_host_key = new Bytes (reader.read_member ("remoteUnlockHostKey").get_data_value ());
reader.end_member ();
} else {
_remote_unlock_host_key = null;
}
}
private void on_state_changed (Object obj, ParamSpec pspec) {
@@ -224,7 +232,7 @@ namespace Frida.Fruity {
tunnel_request = new Promise<Tunnel> ();
try {
var tunnel = new MacOSTunnel (pairing_device);
var tunnel = new MacOSTunnel (pairing_device, _remote_unlock_host_key);
yield tunnel.attach (cancellable);
tunnel_request.resolve (tunnel);
@@ -252,6 +260,12 @@ namespace Frida.Fruity {
}
}
public Bytes? remote_unlock_host_key {
get {
return _remote_unlock_host_key;
}
}
public Darwin.Xpc.Uuid? assertion_identifier {
get {
return _assertion_identifier;
@@ -259,13 +273,15 @@ namespace Frida.Fruity {
}
private XpcClient pairing_device;
private Bytes? _remote_unlock_host_key;
private Darwin.Xpc.Uuid? _assertion_identifier;
private InetAddress? tunnel_device_address;
private DiscoveryService? _discovery;
private int64 _opened_at = -1;
public MacOSTunnel (XpcClient pairing_device) {
public MacOSTunnel (XpcClient pairing_device, Bytes? remote_unlock_host_key) {
this.pairing_device = pairing_device;
_remote_unlock_host_key = remote_unlock_host_key;
}
public async void close (Cancellable? cancellable) throws IOError {
+23 -1
View File
@@ -247,6 +247,9 @@ namespace Frida.Fruity {
checkin.set_string ("Request", "RSDCheckin");
checkin.set_string ("Label", "Xcode");
checkin.set_string ("ProtocolVersion", "2");
unowned Bytes? key = tunnel.remote_unlock_host_key;
if (key != null)
checkin.set_bytes ("EscrowBag", key);
try {
yield service.query (checkin, cancellable);
@@ -496,6 +499,10 @@ namespace Frida.Fruity {
get;
}
public abstract Bytes? remote_unlock_host_key {
get;
}
public abstract async void close (Cancellable? cancellable) throws IOError;
public abstract async IOStream open_tcp_connection (uint16 port, Cancellable? cancellable) throws Error, IOError;
}
@@ -1675,10 +1682,17 @@ namespace Frida.Fruity {
}
}
public Bytes? remote_unlock_host_key {
get {
return _remote_unlock_host_key;
}
}
private UsbNcmDriver? ncm;
private TunnelConnection? tunnel_connection;
private DiscoveryService? _discovery_service;
private int64 _opened_at = -1;
private Bytes? _remote_unlock_host_key;
public PortableUsbTunnel (UsbDevice device, NcmPeer peer, PairingStore store) {
Object (
@@ -1707,11 +1721,11 @@ namespace Frida.Fruity {
);
var pairing_transport = new XpcPairingTransport (yield netstack.open_tcp_connection (tunnel_endpoint, cancellable));
var pairing_service = yield PairingService.open (pairing_transport, pairing_store, cancellable);
TunnelConnection tc = yield pairing_service.open_tunnel (ncm_peer.ip, netstack, cancellable);
tc.closed.connect (on_tunnel_connection_close);
_opened_at = get_monotonic_time ();
_remote_unlock_host_key = pairing_service.established_peer.remote_unlock_host_key;
var rsd_endpoint = (InetSocketAddress) Object.new (typeof (InetSocketAddress),
address: tc.remote_address,
@@ -1880,9 +1894,16 @@ namespace Frida.Fruity {
}
}
public Bytes? remote_unlock_host_key {
get {
return _remote_unlock_host_key;
}
}
private TunnelConnection? tunnel_connection;
private DiscoveryService? _discovery_service;
private int64 _opened_at = -1;
private Bytes? _remote_unlock_host_key;
private const uint PAIRING_CONNECTION_TIMEOUT = 2000;
@@ -1905,6 +1926,7 @@ namespace Frida.Fruity {
TunnelConnection tc = yield pairing_service.open_tunnel (endpoint.get_address (), netstack, cancellable);
_opened_at = get_monotonic_time ();
_remote_unlock_host_key = pairing_service.established_peer.remote_unlock_host_key;
var rsd_endpoint = (InetSocketAddress) Object.new (typeof (InetSocketAddress),
address: tc.remote_address,
+148 -15
View File
@@ -146,6 +146,11 @@ namespace Frida.Fruity {
private set;
}
public PairingPeer? established_peer {
get;
private set;
}
private Gee.Map<uint64?, Promise<ObjectReader>> requests =
new Gee.HashMap<uint64?, Promise<ObjectReader>> (Numeric.uint64_hash, Numeric.uint64_equal);
private uint64 next_control_sequence_number = 0;
@@ -181,15 +186,35 @@ namespace Frida.Fruity {
yield attempt_pair_verify (cancellable);
Bytes? shared_key = yield verify_manual_pairing (cancellable);
if (shared_key == null) {
PairingPeer? peer = yield verify_manual_pairing (cancellable);
if (peer == null) {
if (!device_options.allows_pair_setup)
throw new Error.NOT_SUPPORTED ("Device not paired and pairing not allowed on current transport");
shared_key = yield setup_manual_pairing (cancellable);
peer = yield setup_manual_pairing (cancellable);
}
client_cipher = new ChaCha20Poly1305 (derive_chacha_key (shared_key, "ClientEncrypt-main"));
server_cipher = new ChaCha20Poly1305 (derive_chacha_key (shared_key, "ServerEncrypt-main"));
if (peer.remote_unlock_host_key == null) {
bool peer_modified = false;
try {
peer.remote_unlock_host_key = yield create_remote_unlock_key (cancellable);
peer_modified = true;
} catch (Error e) {
if (e is Error.INVALID_OPERATION) {
store.forget_peer (peer);
throw e;
}
if (!(e is Error.NOT_SUPPORTED))
throw e;
}
if (peer_modified) {
try {
store.save_peer (peer);
} catch (Error e) {
}
}
}
established_peer = peer;
return true;
}
@@ -377,7 +402,7 @@ namespace Frida.Fruity {
}
}
private async Bytes? verify_manual_pairing (Cancellable? cancellable) throws Error, IOError {
private async PairingPeer? verify_manual_pairing (Cancellable? cancellable) throws Error, IOError {
Key host_keypair = make_keypair (X25519);
uint8[] raw_host_pubkey = get_raw_public_key (host_keypair).get_data ();
@@ -414,10 +439,18 @@ namespace Frida.Fruity {
var cipher = new ChaCha20Poly1305 (operation_key);
// TODO: Verify signature using peer's public key.
/* var start_inner_response = */ new VariantReader (PairingParamsParser.parse (cipher.decrypt (
var start_inner_response = new VariantReader (PairingParamsParser.parse (cipher.decrypt (
new Bytes.static ("\x00\x00\x00\x00PV-Msg02".data[:12]),
start_response.read_member ("encrypted-data").get_data_value ())));
string peer_identifier = start_inner_response
.read_member ("identifier")
.get_uuid_value ();
PairingPeer? peer = store.find_peer_by_identifier (peer_identifier);
if (peer == null) {
yield notify_pair_verify_failed (cancellable);
return null;
}
// TODO: Verify signature using peer's public key.
unowned string host_identifier = store.self_identity.identifier;
@@ -457,7 +490,9 @@ namespace Frida.Fruity {
return null;
}
return shared_key;
setup_main_encryption_keys (shared_key);
return peer;
}
private async void notify_pair_verify_failed (Cancellable? cancellable) throws Error, IOError {
@@ -476,7 +511,7 @@ namespace Frida.Fruity {
.build (), cancellable);
}
private async Bytes setup_manual_pairing (Cancellable? cancellable) throws Error, IOError {
private async PairingPeer setup_manual_pairing (Cancellable? cancellable) throws Error, IOError {
Bytes start_params = new PairingParamsBuilder ()
.add_method (0)
.add_state (1)
@@ -612,9 +647,78 @@ namespace Frida.Fruity {
inner_finish_response.end_member ();
Bytes peer_info = inner_finish_response.read_member ("info").get_data_value ();
inner_finish_response.end_member ();
store.add_peer (peer_identifier, peer_pubkey, peer_info);
return shared_key;
PairingPeer peer = store.add_peer (peer_identifier, peer_pubkey, peer_info);
setup_main_encryption_keys (shared_key);
return peer;
}
private async Bytes? create_remote_unlock_key (Cancellable? cancellable) throws Error, IOError {
string request = Json.to_string (
new Json.Builder ()
.begin_object ()
.set_member_name ("request")
.begin_object ()
.set_member_name ("_0")
.begin_object ()
.set_member_name ("createRemoteUnlockKey")
.begin_object ()
.end_object ()
.end_object ()
.end_object ()
.end_object ()
.get_root (), false);
string response = yield request_encrypted (request, cancellable);
Json.Reader reader;
try {
reader = new Json.Reader (Json.from_string (response));
} catch (GLib.Error e) {
throw new Error.PROTOCOL ("Invalid createRemoteUnlockKey response JSON");
}
reader.read_member ("response");
reader.read_member ("_1");
if (reader.read_member ("errorExtended")) {
reader.read_member ("_0");
reader.read_member ("domain");
unowned string? domain = reader.get_string_value ();
reader.end_member ();
reader.read_member ("code");
int64 code = reader.get_int_value ();
reader.end_member ();
reader.read_member ("userInfo");
reader.read_member ("NSLocalizedDescription");
unowned string? description = reader.get_string_value ();
if (domain == null || description == null)
throw new Error.PROTOCOL ("Invalid createRemoteUnlockKey error response");
if (domain == "com.apple.CoreDevice.ControlChannelConnectionError" && code == 2)
throw new Error.INVALID_OPERATION ("%s", description);
throw new Error.NOT_SUPPORTED ("%s", description);
}
reader.end_member ();
reader.read_member ("createRemoteUnlockKey");
reader.read_member ("hostKey");
unowned string? key = reader.get_string_value ();
if (key == null)
throw new Error.PROTOCOL ("Malformed createRemoteUnlockKey response");
return new Bytes (Base64.decode (key));
}
private void setup_main_encryption_keys (Bytes shared_key) {
client_cipher = new ChaCha20Poly1305 (derive_chacha_key (shared_key, "ClientEncrypt-main"));
server_cipher = new ChaCha20Poly1305 (derive_chacha_key (shared_key, "ServerEncrypt-main"));
}
private async ObjectReader request_pairing_data (Bytes payload, Cancellable? cancellable) throws Error, IOError {
@@ -1383,7 +1487,7 @@ namespace Frida.Fruity {
_peers = load_peers ();
}
public void add_peer (string identifier, Bytes public_key, Bytes info) throws Error {
public PairingPeer add_peer (string identifier, Bytes public_key, Bytes info) throws Error {
var r = new VariantReader (OpackParser.parse (info));
Bytes irk = r.read_member ("altIRK").get_data_value ();
@@ -1406,6 +1510,7 @@ namespace Frida.Fruity {
model = model,
udid = udid,
info = info,
remote_unlock_host_key = null,
};
_peers.add (peer);
@@ -1413,6 +1518,12 @@ namespace Frida.Fruity {
save_peer (peer);
} catch (Error e) {
}
return peer;
}
public PairingPeer? find_peer_by_identifier (string identifier) {
return peers.first_match (p => p.identifier == identifier);
}
public PairingPeer? find_peer_matching_service (PairingServiceDetails service) {
@@ -1480,6 +1591,10 @@ namespace Frida.Fruity {
var r = new VariantReader (OpackParser.parse (info));
unowned string udid = r.read_member ("remotepairing_udid").get_string_value ();
Bytes? remote_unlock_host_key = null;
if (plist.has ("remoteUnlockHostKey"))
remote_unlock_host_key = plist.get_bytes ("remoteUnlockHostKey");
peers.add (new PairingPeer () {
identifier = plist.get_string ("identifier"),
public_key = plist.get_bytes ("publicKey"),
@@ -1488,6 +1603,7 @@ namespace Frida.Fruity {
model = plist.get_string ("model"),
udid = udid,
info = info,
remote_unlock_host_key = remote_unlock_host_key,
});
}
} catch (GLib.Error e) {
@@ -1496,7 +1612,7 @@ namespace Frida.Fruity {
return peers;
}
private static void save_peer (PairingPeer peer) throws Error {
public void save_peer (PairingPeer peer) throws Error {
var plist = new Plist ();
plist.set_string ("identifier", peer.identifier);
plist.set_bytes ("publicKey", peer.public_key);
@@ -1504,13 +1620,29 @@ namespace Frida.Fruity {
plist.set_string ("name", peer.name);
plist.set_string ("model", peer.model);
plist.set_bytes ("info", peer.info);
save_plist (plist, query_peers_location ().get_child (peer.identifier + ".plist"));
if (peer.remote_unlock_host_key != null)
plist.set_bytes ("remoteUnlockHostKey", peer.remote_unlock_host_key);
save_plist (plist, query_peer_location (peer));
}
public void forget_peer (PairingPeer peer) {
try {
query_peer_location (peer).delete ();
} catch (GLib.Error e) {
}
_peers.remove (peer);
}
private static File query_self_identity_location () {
return query_base_location ().get_child ("self-identity.plist");
}
private static File query_peer_location (PairingPeer peer) {
return query_peers_location ().get_child (peer.identifier + ".plist");
}
private static File query_peers_location () {
return query_base_location ().get_child ("peers");
}
@@ -1557,6 +1689,7 @@ namespace Frida.Fruity {
public string model;
public string udid;
public Bytes info;
public Bytes? remote_unlock_host_key;
}
public class PairingServiceMetadata {