-
-
Save Jonahss/283c213d348663381efa to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Very alpha still, but getting there... | |
# Yeah, I like it this way | |
require 'pp' | |
require 'set' | |
require 'zlib' | |
require 'base64' | |
require 'socket' | |
require 'openssl' | |
require 'stringio' | |
require 'net/http' | |
require 'net/https' | |
require 'osx/cocoa' | |
require 'rexml/document' | |
begin | |
require 'usb' | |
$usb_available = true | |
rescue | |
$usb_available = false | |
end | |
Thread.abort_on_exception = true | |
class String | |
def hexdump | |
buffer = "" | |
(0..length).step(16) do |i| | |
first_half = self[i, 8].unpack("C*").map{|x| "%02x" % x}.join(" ") | |
second_half = (self[i + 8, 8] || "").unpack("C*").map{|x| "%02x" % x}.join(" ") | |
first_half += " " * (8 * 2 + (8 - 1) - first_half.length) | |
second_half += " " * (8 * 2 + (8 - 1) - second_half.length) | |
buffer += first_half + " " + second_half + "\t" + self[i, 16].unpack("C*").map{|x| ((32..128).include?(x) ? x : ?.).chr}.join + "\n" | |
end | |
puts buffer | |
end | |
def printable? | |
self.unpack("C*").all?{|x| ((32..128).entries + [9, 10]).include?(x)} | |
end | |
def to_hex | |
self.unpack("C*").map{|x| "%02x" % x}.join | |
end | |
end | |
class Integer | |
@@host_is_little = "\x01\x02\x03\x04".unpack("N4")[0] == 0x01020304 | |
def byteswap | |
value = 0 | |
self.size.times do |i| | |
value |= (self & (0xFF << (8 * i))) << (8 * ((self.size - 1) - (2 * i))) | |
end | |
value | |
end | |
def big_to_host | |
@@host_is_little ? self.byteswap : self | |
end | |
def little_to_host | |
@@host_is_little ? self : self.byteswap | |
end | |
end | |
class PList | |
attr_reader :object | |
def initialize(data) | |
@object = data.printable? ? decode_xml_plist(data) : decode_binary_plist(data) | |
end | |
def get_int(size) | |
value = 0 | |
size.times{value = (value << 8) | @io.read(1).unpack("C")[0]} | |
value | |
end | |
def check_int | |
marker = @io.read(1).unpack("C")[0] | |
raise "Int expected" unless marker & 0xf0 == 0x10 | |
get_int(1 << (marker & 0xf)) | |
end | |
def read_object | |
marker = @io.read(1).unpack("C")[0] | |
case marker & 0xf0 | |
when 0x00 # Null | |
case marker & 0x0f | |
when 0: nil | |
when 8: false | |
when 9: true | |
when 15: "<fill>" | |
end | |
when 0x10 # Int | |
get_int(1 << marker & 0x0f) | |
when 0x20: "**** Real ****" | |
when 0x30: "**** Date ****" | |
when 0x40, 0x50 # Data / ASCII | |
size = marker & 0xf | |
size = check_int if size == 15 | |
@io.read(size) | |
when 0x60 # Unicode16 | |
when 0x80 # UID | |
when 0xa0 # Array | |
size = marker & 0x0f | |
size = check_int if size == 15 | |
value_indices = [] | |
size.times {value_indices << get_int(@offset_int_size)} | |
value_indices.map {|i| @io.seek(@offset_table[i]); read_object} | |
when 0xd0 # dict | |
size = marker & 0xf | |
size = check_int if size == 15 | |
key_indices = [] | |
value_indices = [] | |
size.times {key_indices << get_int(@offset_int_size)} | |
size.times {value_indices << get_int(@offset_int_size)} | |
keys = key_indices.map{|i| @io.seek(@offset_table[i]); read_object} | |
values = value_indices.map{|i| @io.seek(@offset_table[i]); read_object} | |
keys.zip(values).inject({}){|accum, pair| accum[pair[0]] = pair[1]; accum} | |
end | |
end | |
def decode_binary_plist(data) | |
@io = StringIO.new(data) | |
raise "Invalid binary plist" unless @io.read(6) == "bplist" && (0..2).member?(@io.read(2).to_i ) | |
@io.seek(-32, IO::SEEK_END) | |
unused, @offset_int_size, @object_ref_size, @num_objects, @top_object, @offset_table_offset = @io.read(32).unpack("a6CCQQQ") | |
@num_objects = @num_objects.big_to_host | |
@top_object = @top_object.big_to_host | |
@offset_table_offset = @offset_table_offset.big_to_host | |
@io.seek(@offset_table_offset) | |
@offset_table = [] | |
@num_objects.times{@offset_table << get_int(@offset_int_size)} | |
@io.seek(@offset_table[@top_object]) | |
read_object | |
end | |
def decode_xml_element(element) | |
return decode_xml_element(element.next_sibling_node) if element.class == REXML::Text && element.value.strip.empty? | |
case element.name | |
when "plist": decode_xml_element(element.children[0]) | |
when "dict" | |
hash = {} | |
element.each_child do |child| | |
if child.kind_of?(REXML::Element) && child.name == "key" | |
hash[child.children[0].value] = decode_xml_element(child.next_sibling_node) | |
end | |
end | |
hash | |
when "array" | |
array = [] | |
element.each_child{|child| array << decode_xml_element(child) if child.kind_of?(REXML::Element)} | |
array | |
when "integer": element.children[0].value.to_i | |
when "real": element.children[0].value.to_f | |
when "string": element.children[0].value | |
when "data": Base64.decode64(element.children[0].value) | |
when "true": true | |
when "false": false | |
end | |
end | |
def decode_xml_plist(data) | |
decode_xml_element(REXML::Document.new(data).root) | |
end | |
end | |
class CPIO | |
def initialize(input) | |
@io = input.kind_of?(String) ? StringIO.new(input) : input | |
while true do | |
magic, dev, ino, mode, uid, gid, nlink, rdev, mtime, namesize, filesize = @io.read(76).unpack("a6a6a6a6a6a6a6a6a11a6a11") | |
# Weird format uses octal ascii? | |
file_name = @io.read(namesize.to_i(8)).chop | |
file_data = @io.read(filesize.to_i(8)) | |
break if file_name == "TRAILER!!!" | |
puts file_name | |
end | |
end | |
end | |
class SocketRelay | |
def self.[](a, b) | |
self.new(a, b) | |
end | |
def initialize(a, b) | |
@a, @b = a, b | |
# TODO: This should be done with a select | |
@a_to_b = Thread.new do | |
while true | |
value = @a.readpartial(4096) rescue break | |
p value | |
@b.write(value) | |
end | |
end | |
@b_to_a = Thread.new do | |
while true | |
value = @b.readpartial(4096) rescue break | |
p value | |
@a.write(value) | |
end | |
end | |
end | |
end | |
class USBTCPSocket | |
attr_reader :device_id, :product_id, :serial_no | |
def send_packet(socket, packet_type, data) | |
packet = [data.length + 16, 0, packet_type, @session_number].pack("V4") + data | |
socket.write(packet) | |
end | |
def recv_packet(socket) | |
header = socket.read(16) | |
packet_length, unk, packet_type, tag = header.unpack("V4") | |
data = socket.read(packet_length - 16) | |
[packet_type, tag, data] | |
end | |
def initialize(port, override_device_id = nil) | |
@session_number = 1 | |
if override_device_id | |
@device_id = override_device_id | |
else | |
# This just grabs the device id and serial number. Once you've run the hello protocol it won't accept anything else | |
socket = UNIXSocket.new("/var/run/usbmuxd") | |
puts "Sending hello packet" | |
send_packet(socket, 3, "") | |
p recv_packet(socket) | |
@device_id, @product_id, @serial_no = recv_packet(socket)[2].unpack("Vva40") | |
end | |
puts "Device ID: 0x#{@device_id.to_s(16)}" | |
@use_ssl = false | |
done = false | |
until done do | |
@socket = UNIXSocket.new("/var/run/usbmuxd") | |
puts "Retrying connection to port #{port}..." | |
send_packet(@socket, 2, [device_id, port, 0].pack("Vnn")) | |
response = recv_packet(@socket)[2] | |
p response | |
done = response.unpack("N")[0] == 0 | |
@socket.close unless done | |
@session_number += 1 | |
end | |
puts "Connected to port #{port}" | |
end | |
def use_ssl=(value) | |
if value && !@use_ssl | |
@use_ssl = true | |
@plain_socket = @socket | |
@socket = OpenSSL::SSL::SSLSocket.new(@plain_socket, OpenSSL::SSL::SSLContext.new(:TLSv1)) | |
@socket.connect | |
elsif !value && @use_ssl | |
@use_ssl = false | |
@socket.close | |
@socket = @plain_socket | |
end | |
end | |
def write(data) | |
@socket.write(data) | |
end | |
def read(size) | |
@socket.read(size) | |
end | |
def gets(separator = $/) | |
@socket.gets(separator) | |
end | |
def readpartial(size) | |
@socket.readpartial(size) | |
end | |
def close | |
@socket.close | |
end | |
end | |
class Service | |
def self.handle(type) | |
(@@handlers ||= Hash.new{|hash, key| hash[key] = USBTCPSocket})[type] = self | |
end | |
def self.[](type) | |
@@handlers[type] | |
end | |
def self.all | |
@@handlers.keys | |
end | |
end | |
class PropertyListService < Service | |
def initialize(port, device_id = nil) | |
@socket = USBTCPSocket.new(port, device_id) | |
end | |
def request_plist(data, format = OSX::NSPropertyListXMLFormat_v1_0) | |
write_plist(data, format) | |
read_plist | |
end | |
def write_plist(data, format = OSX::NSPropertyListXMLFormat_v1_0) | |
if data.kind_of?(Hash) || data.kind_of?(Array) | |
data = data.to_plist | |
end | |
@socket.write([data.length].pack("N") + data) | |
end | |
def read_plist | |
size = @socket.read(4).unpack("N")[0] | |
buffer = @socket.read(size) | |
OSX.load_plist(buffer) | |
end | |
end | |
class PortForwarderService < Service | |
def initialize(port) | |
super(port) | |
# TODO: fill me in | |
end | |
end | |
class LockdownService < PropertyListService | |
handle "com.apple.mobile.lockdown" | |
def initialize(port, label = "iTunes", host_id = nil, pair_record_path = nil) | |
super(port) | |
@label = label | |
pair_record_path ||= "#{File.expand_path("~")}/Library/Lockdown/#{@socket.serial_no}.plist" | |
raise "QueryType request failed" unless query_type["Result"] == "Success" | |
raise "Not talking to lockdownd! #{query_type["Type"]} instead!" unless query_type["Type"] == "com.apple.mobile.lockdown" | |
pair_record = Hash[*OSX.load_plist(File.read(pair_record_path)).select{|key, value| %w(DeviceCertificate HostCertificate HostID RootCertificate).include?(key)}.map{|key, value| [key, value.respond_to?(:rubyString) ? value.rubyString : value]}.flatten] | |
raise "Unable to validate pairing records" unless validate_pair(pair_record)["Result"] == "Success" | |
raise "Unable to start session" unless start_session(pair_record["HostID"])["Result"] == "Success" | |
end | |
def request(type, hash = {}) | |
request_plist({"Label" => @label, "Request" => type}.merge(hash)) | |
end | |
# The get value calls seem to return meaningful values for the following domains: | |
# com.apple.mobile.lockdown | |
# com.apple.mobile.iTunes | |
# com.apple.mobile.battery | |
# com.apple.springboard.curvedBatteryCapacity | |
# com.apple.mobile.internal | |
# com.apple.mobile.debug | |
# com.apple.mobile.restriction | |
# com.apple.mobile.sync_data_class | |
# com.apple.mobile.data_sync | |
# com.apple.mobile.nikita | |
# com.apple.fairplay | |
# com.apple.international | |
# com.apple.disk_usage | |
# and after xcode has done stuff: com.apple.xcode.developerdomain | |
def get_value(key, domain = "") | |
request("GetValue", {"Key" => key}.merge(domain == "" ? {} : {"Domain" => domain}))["Value"] | |
end | |
def pair(root_certificate, device_certificate, host_certificate, host_id) | |
request("Pair", "PairRecord" => { | |
"DeviceCertificate" => device_certificate, | |
"HostCertificate" => host_certificate, | |
"HostID" => host_id, | |
"RootCertificate" => root_certificate | |
}) | |
end | |
def unpair | |
request("Unpair") | |
end | |
def activate(activation_record) | |
request("Activate", "ActivationRecord" => activation_record) | |
end | |
def deactivate | |
request("Deactivate") | |
end | |
def get_values(domain = "") | |
# TODO: check for success | |
request("GetValue", domain == "" ? {} : {"Domain" => domain})["Value"] | |
end | |
def query_type | |
# Might as well cache this one | |
@query_type ||= request("QueryType") | |
end | |
def validate_pair(pair_record) | |
request("ValidatePair", "PairRecord" => pair_record) | |
end | |
def enter_recovery | |
request("EnterRecovery") | |
end | |
def start_session(host_id) | |
response = request("StartSession", "HostID" => host_id) | |
@socket.use_ssl = response["EnableSessionSSL"] | |
response | |
end | |
def start_service(service_name, handler = nil) | |
response = request("StartService", "Service" => service_name) | |
raise "Unable to start service #{service_name}" unless response["Result"] == "Success" | |
(handler || Service[service_name]).new(response["Port"]) | |
end | |
end | |
class DeviceLinkService < PropertyListService | |
def initialize(port) | |
super(port) | |
version_exchange = read_plist | |
raise "Version exchange message expected" unless version_exchange[0] == "DLMessageVersionExchange" | |
response = request_plist(["DLMessageVersionExchange", "DLVersionsOk"], OSX::NSPropertyListBinaryFormat_v1_0) | |
raise "Device not ready" unless response[0] == "DLMessageDeviceReady" | |
end | |
def close | |
write_plist(["DLMessageDisconnect", "All good things must come to an end."]) | |
end | |
end | |
class ScreenShotService < DeviceLinkService | |
handle "com.apple.mobile.screenshotr" | |
def take_screenshot | |
response = request_plist(["DLMessageProcessMessage", {"MessageType" => "ScreenShotRequest"}]) | |
raise "Invalid reply type" unless response[0] == "DLMessageProcessMessage" | |
raise "Invalid reply message type: #{response[1]["MessageType"]}" unless response[1]["MessageType"] == "ScreenShotReply" | |
response[1]["ScreenShotData"].rubyString | |
end | |
end | |
class CrashReportService < DeviceLinkService | |
handle "com.apple.crashreportcopy" | |
def system_info | |
request_plist(["DLMessageProcessMessage", {"VersionNumber" => 101, "CrashReportRequest" => "GetSysInfo"}]) | |
end | |
end | |
class ImageMounterService < PropertyListService | |
handle "com.apple.mobile.mobile_image_mounter" | |
def lookup_image(image_type) | |
request("LookupImage", "ImageType" => "Developer") | |
end | |
def mount_image(path, signature, type) | |
response = request("MountImage", "ImagePath" => path, "ImageSignature" => signature, "ImageType" => type) | |
end | |
def close | |
request("Hangup") | |
end | |
def request(command, hash = {}) | |
request_plist({"Command" => command}.merge(hash)) | |
end | |
end | |
class NotificationProxyService < PropertyListService | |
handle "com.apple.mobile.notification_proxy" | |
attr_reader :listener | |
def initialize(port) | |
super(port) | |
@observers = Hash.new{|hash, key| hash[key] = Set[]} | |
@listener = Thread.start do | |
while true | |
plist = read_plist | |
if plist["Command"] == "RelayNotification" | |
@observers[plist["Name"]].each do |handler| | |
handler.call | |
end | |
end | |
end | |
end | |
end | |
# Valid notifications include: | |
# com.apple.language.changed | |
# com.apple.AddressBook.PreferenceChanged | |
# com.apple.mobile.data_sync.domain_changed | |
# com.apple.mobile.lockdown.device_name_changed | |
# com.apple.mobile.developer_image_mounted | |
# com.apple.mobile.lockdown.trusted_host_attached | |
# com.apple.mobile.lockdown.host_detached | |
# com.apple.mobile.lockdown.host_attached | |
# com.apple.mobile.lockdown.phone_number_changed | |
# com.apple.mobile.lockdown.registration_failed | |
# com.apple.mobile.lockdown.activation_state | |
# com.apple.mobile.lockdown.brick_state | |
# com.apple.mobile.lockdown.developer_status_changed | |
# com.apple.itunes-client.syncCancelRequest | |
# com.apple.itunes-client.syncSuspendRequest | |
# com.apple.itunes-client.syncResumeRequest | |
# com.apple.springboard.attemptactivation | |
# com.apple.springboard.deviceWillShutDown | |
# com.apple.mobile.application_installed | |
# com.apple.mobile.application_uninstalled | |
def observe(name, &block) | |
write_plist("Command" => "ObserveNotification", "Name" => name) | |
@observers[name] << block | |
end | |
def post(name) | |
write_plist("Command" => "PostNotification", "Name" => name) | |
end | |
end | |
class InstallationProxyService < PropertyListService | |
handle "com.apple.mobile.installation_proxy" | |
def browse(options = {}) | |
result = request("Browse", options) | |
done = false | |
until done do | |
plist = read_plist | |
done = plist["Status"] == "Complete" | |
end | |
# TODO: make it work better | |
result | |
end | |
def install(package_path, type = "User", &progress_callback) | |
request("Install", "PackagePath" => package_path, "ClientOptions" => {"PackageType" => type}) | |
done = false | |
until done do | |
plist = read_plist | |
done = plist["Status"] == "Complete" | |
end | |
end | |
def request(command, hash = {}) | |
request_plist({"Command" => command}.merge(hash)) | |
end | |
end | |
class SyslogRelayService < Service | |
handle "com.apple.syslog_relay" | |
attr_reader :listener | |
def initialize(port) | |
@socket = USBTCPSocket.new(port) | |
@syslog = "" | |
@observers = Set[] | |
@listener = Thread.new do | |
while true | |
# syslog_relay will send at most 16383 bytes at a time | |
buffer = @socket.readpartial(16383) | |
@observers.each{|observer| observer.call(buffer)} | |
@syslog << buffer | |
end | |
end | |
end | |
def observe(&block) | |
@observers << block | |
end | |
end | |
class MISService < PropertyListService | |
handle "com.apple.misagent" | |
def initialize(port) | |
super(port) | |
end | |
# Only provisioning profiles seem to be supported in misagent for now (as of 5a345) | |
def install_profile(profile, profile_type = "Provisioning") | |
request("Install", "Profile" => profile, "ProfileType" => profile_type) | |
end | |
# Fixme: What does this do? | |
def copy_profile(profile_type = "Provisioning") | |
request("Copy", "ProfileType" => profile_type) | |
end | |
def remove_profile(profile_id, profile_type = "Provisioning") | |
request("Remove", "ProfileID" => profile_id, "ProfileType" => profile_type) | |
end | |
def request(message_type, hash = {}) | |
request_plist({"MessageType" => message_type}.merge(hash)) | |
end | |
end | |
class ApplistService < Service | |
handle "com.apple.debugserver.applist" | |
def initialize(port) | |
@socket = USBTCPSocket.new(port) | |
end | |
def applist | |
# Fixme: Hardcoded | |
OSX.load_plist(@socket.readpartial(128 * 1024)) | |
end | |
end | |
class FileRelayService < PropertyListService | |
handle "com.apple.mobile.file_relay" | |
# The following are valid sources | |
# AppleSupport | |
# Network | |
# VPN | |
# WiFi | |
# UserDatabases | |
# CrashReporter | |
# tmp | |
# SystemConfiguration | |
def request_source(*sources) | |
result = request_plist("Sources" => sources.flatten) | |
raise "Unable to retrieve sources" unless result["Status"] == "Acknowledged" | |
gzip_reader = Zlib::GzipReader.new(@socket) | |
CPIO.new(gzip_reader) | |
end | |
end | |
class BackupService < DeviceLinkService | |
handle "com.apple.mobilebackup" | |
def initialize(port) | |
super(port) | |
# TODO: finish me | |
["DLMessageProcessMessage", {"BackupMessageTypeKey" => "BackupMessageBackupRequest", | |
"BackupComputerBasePathKey"=>"/", | |
"BackupManifestKey" => | |
{"Data" => "moo", | |
"AuthVersion" => "1.0", | |
"AuthData" => "Forty Two", | |
"Auth Signature" => "SHA1"}}] | |
end | |
end | |
class AFCService < Service | |
handle "com.apple.afc" | |
handle "com.apple.afc2" | |
class AFCIO | |
def write(data) | |
end | |
def read(size) | |
end | |
end | |
def initialize(port) | |
@socket = USBTCPSocket.new(port) | |
@sequence_number = 0 | |
end | |
def send_frame(type, data = "", header_size = data.length + 40) | |
frame = "CFA6LPAA" + [data.length + 40, 0, header_size, 0, @sequence_number += 1, 0, type, 0].pack("V*") + data | |
frame.hexdump | |
@socket.write(frame) | |
end | |
def recv_frame() | |
magic, size = @socket.read(16).unpack("a8V*") | |
raise "Invalid frame" unless magic == "CFA6LPAA" | |
header_size = @socket.read(8).unpack("V")[0] # Ignoring other 4 bytes | |
sequence_number = @socket.read(8).unpack("V")[0] # Ignoring other 4 bytes | |
header_rest = @socket.read(header_size - 32) | |
@socket.read(size - header_size) | |
end | |
def sysinfo() | |
send_frame(11) | |
Hash[*recv_frame().split("\x00").map{|x| x.to_i.to_s == x ? x.to_i : x}] | |
end | |
def stat(path) | |
send_frame(10, path + "\x00") | |
Hash[*recv_frame().split("\x00").map{|x| x.to_i.to_s == x ? x.to_i : x}] | |
end | |
def mkdir(path) | |
send_frame(9, path + "\x00") | |
recv_frame() | |
end | |
def ls(path) | |
send_frame(3, path + "\x00") | |
recv_frame.split("\x00") | |
end | |
# 2 appears to be readable, 3 seems to create the file | |
def open(path, mode = 2) | |
send_frame(13, [mode, 0].pack("V*") + path + "\x00") | |
recv_frame | |
end | |
def read(file_size) | |
send_frame(15, [1, 0, file_size, 0].pack("V*")) | |
recv_frame | |
end | |
def write(data) | |
send_frame(16, [1, 0].pack("V*") + data, 48) | |
end | |
end | |
class RestoreService < PropertyListService | |
handle "com.apple.mobile.restored" | |
def reboot | |
request("Reboot") | |
end | |
def start_restore(progress_callback = nil, &data_request_handler) | |
write_plist("Request" => "StartRestore", "RestoreProtocolVersion" => 11) | |
p "wrote plist" | |
while plist = read_plist do | |
p "got plist" | |
p plist | |
if plist["MsgType"] =="DataRequestMsg" | |
response = data_request_handler.call(plist["DataType"]) | |
write_plist(response) if response | |
elsif progress_callback && plist["MsgType"] == "ProgressMsg" | |
progress_callback.call(plist["Operation"], plist["Progress"]) | |
elsif plist["MsgType"] == "StatusMsg" | |
puts "Got status message: #{plist.inspect}" | |
break if plist["Status"] == 0 | |
end | |
end | |
end | |
def goodbye | |
request("Goodbye") | |
end | |
# Valid keys (key cannot be empty): | |
# SerialNumber | |
# IMEI | |
# HardwareModel | |
def query_value(key) | |
request("QueryValue", "QueryKey" => key) | |
end | |
def query_type | |
request("QueryType") | |
end | |
def request(command, hash = {}) | |
request_plist({"Request" => command}.merge(hash)) | |
end | |
end | |
class ASRService < Service | |
def initialize(port, input) | |
@socket = USBTCPSocket.new(port) | |
if input.kind_of?(File) | |
@io = input | |
@size = input.stat.size | |
elsif input.kind_of?(String) | |
@io = StringIO.new(input) | |
@size = input.size | |
end | |
raise "Unexpected command" unless read_plist["Command"] == "Initiate" | |
end | |
def start | |
write_plist({ | |
"FEC Slice Stride" => 40, | |
"Packet Payload Size" => 1450, | |
"Packets Per FEC" => 25, | |
"Payload" => { | |
"Port" => 1, | |
"Size" => @size | |
}, | |
"Stream ID" => 1, | |
"Version" => 1 | |
}) | |
while plist = read_plist do | |
if plist["Command"] == "OOBData" | |
size = plist["OOB Length"] | |
offset = plist["OOB Offset"] | |
puts "Sending #{size} OOB bytes from offset #{offset}" | |
@io.seek(offset) | |
@socket.write(@io.read(size)) | |
elsif plist["Command"] == "Payload" | |
puts "Sending payload" | |
@io.seek(0) | |
index = 0 | |
while buffer = @io.read(0x10000) do | |
@socket.write(buffer) | |
index += 1 | |
if index % 16 == 0 | |
puts "#{index.to_f / (@size / 0x10000) * 100}% done" | |
end | |
end | |
break | |
else | |
puts "Unknown ASR command #{plist.inspect}" | |
end | |
end | |
end | |
def read_plist | |
buffer = "" | |
while read_buffer = @socket.gets do | |
puts "Read: #{read_buffer.inspect}" | |
buffer << read_buffer | |
break if read_buffer =~ /<\/plist>/ | |
end | |
OSX.load_plist(buffer) | |
end | |
def write_plist(hash) | |
@socket.write(hash.to_plist) | |
end | |
end | |
class AppleDevice | |
@@matches = {} | |
def self.match(value) | |
[value[:vendor_id]].flatten.each do |vendor_id| | |
(@@matches ||= {})[vendor_id] ||= {} | |
[value[:product_id]].flatten.each do |product_id| | |
@@matches[vendor_id][product_id] = self | |
end | |
end | |
end | |
def self.[](vendor_id, product_id) | |
@@matches[vendor_id][product_id] | |
end | |
def self.available_devices | |
available_ids = {} | |
@@matches.each_pair do |vendor_id, value| | |
value.each_pair do |product_id, klass| | |
available_ids[[vendor_id, product_id]] = klass | |
end | |
end | |
USB.devices.map do |device| | |
available_ids[[device.idVendor, device.idProduct]].new(device) if available_ids.has_key?([device.idVendor, device.idProduct]) | |
end.compact | |
end | |
def initialize(device) | |
@device = device | |
end | |
end | |
class NormalMode < AppleDevice | |
match :vendor_id => 0x5ac, :product_id => [0x1290, 0x1291, 0x1292] # iPhone, iPod Touch, iPhone3G | |
attr_reader :service | |
def initialize(device) | |
super(device) | |
end | |
def open | |
service = PropertyListService.new(62078) | |
@service = Service[service.request_plist("Request" => "QueryType")["Type"]].new(62078) | |
end | |
end | |
class RecoveryV1Mode < AppleDevice | |
match :vendor_id => 0x5ac, :product_id => 0x1280 | |
end | |
class RecoveryV2Mode < AppleDevice | |
match :vendor_id => 0x5ac, :product_id => 0x1281 | |
def initialize(device) | |
super(device) | |
end | |
def open | |
@handle = @device.open | |
@handle.set_configuration(@device.configurations[0]) | |
@handle.claim_interface(1) | |
@handle.set_altinterface(1) | |
@receive_buffer = "\x00" * 0x1000 | |
self | |
end | |
def recv_buffer | |
size = @handle.usb_interrupt_read(0x81, @receive_buffer, 0) | |
@receive_buffer[0, size] | |
end | |
def send_command(command) | |
@handle.usb_interrupt_write(0x02, command + "\n", 0) | |
end | |
def send_file(file) | |
@handle.usb_control_msg(0x21, 6, 1, 0, "", 0) | |
index = 0 | |
size = 0 | |
while buffer = file.read(0x1000) do | |
size += buffer.size | |
@handle.usb_control_msg(0x21, 1, index, 0, buffer, 0) | |
index += 1 | |
end | |
@handle.usb_control_msg(0x21, 1, 1, 0, "", 0) | |
buffer = "\x00" * 6 | |
status = @handle.usb_control_msg(0xa1, 3, 0, 0, buffer, 0) | |
buffer.hexdump | |
raise "Bad status" unless buffer[4] == 0x6 | |
status = @handle.usb_control_msg(0xa1, 3, 0, 0, buffer, 0) | |
buffer.hexdump | |
raise "Bad status" unless buffer[4] == 0x7 | |
status = @handle.usb_control_msg(0xa1, 3, 0, 0, buffer, 0) | |
buffer.hexdump | |
raise "Bad status" unless buffer[4] == 0x8 | |
end | |
def close | |
@handle.release_interface(1) | |
@handle.usb_close | |
end | |
end | |
class DFUV2Mode < AppleDevice | |
match :vendor_id => 0x05ac, :product_id => [0x1222, 0x1227] | |
end | |
#pp Service.all | |
x = nil | |
p "finding devices" | |
def pair | |
devs = AppleDevice.available_devices | |
if devs[0].kind_of?(NormalMode) | |
x = devs[0].open | |
root_ca_cert = OpenSSL::X509::Certificate.new | |
root_private_key = OpenSSL::PKey::RSA.new(1024) | |
root_ca_cert.public_key = root_private_key.public_key | |
device_cert = OpenSSL::X509::Certificate.new | |
device_cert.public_key = OpenSSL::PKey::RSA.new(x.get_value("DevicePublicKey").rubyString) | |
device_cert.sign(root_private_key, OpenSSL::Digest::Digest.new("SHA1")) | |
host_private_key = OpenSSL::PKey::RSA.new(1024) | |
host_cert = OpenSSL::X509::Certificate.new | |
host_cert.public_key = host_private_key.public_key | |
host_cert.sign(root_private_key, OpenSSL::Digest::Digest.new("SHA1")) | |
root_pem = root_ca_cert.to_pem | |
device_pem = device_cert.to_pem | |
host_pem = host_cert.to_pem | |
root_data = OSX::NSData.alloc.initWithBytes_length(root_pem, root_pem.size) | |
host_data = OSX::NSData.alloc.initWithBytes_length(host_pem, host_pem.size) | |
device_data = OSX::NSData.alloc.initWithBytes_length(device_pem, device_pem.size) | |
p x.pair(root_data, device_data, host_data, "29942954112249261629052abcdef40") | |
end | |
end | |
def activate | |
devs = AppleDevice.available_devices | |
if devs[0].kind_of?(NormalMode) | |
x = devs[0].open | |
#x.deactivate | |
#abort | |
uid = x.get_value("UniqueDeviceID") | |
imei = x.get_value("InternationalMobileEquipmentIdentity") | |
iccid = x.get_value("IntegratedCircuitCardIdentity") | |
activation_info = x.get_value("ActivationInfo") | |
serial_number = x.get_value("SerialNumber") | |
imsi = x.get_value("InternationalMobileSubscriberIdentity") | |
#p uid | |
#p imei | |
#p iccid | |
#puts activation_info.to_plist | |
#p serial_number | |
#abort | |
url = URI("https://albert.apple.com/WebObjects/ALActivation.woa/wa/iPhoneRegistration") | |
http = Net::HTTP.new(url.host, url.port) | |
http.use_ssl = true | |
http.start do |h| | |
req = Net::HTTP::Post.new(url.path, "User-Agent" => "iTunes/7.7 (Macintosh; U; Intel Mac OS X 10.5.4)") | |
req.form_data = { | |
"AppleSerialNumber" => serial_number, | |
"IMSI" => imsi, | |
"InStoreActivation" => "false", | |
"machineName" => "macos", | |
"activation-info" => activation_info.to_plist, | |
"ICCID" => iccid, | |
"IMEI" => imei | |
} | |
#puts req.body | |
result = h.request(req) | |
puts result.body | |
document = REXML::Document.new(result.body) | |
activation = OSX.load_plist(document.root.elements[1].elements[1].to_s) | |
pp activation | |
activation_record = activation["iphone-activation"]["activation-record"] | |
x.activate(activation_record) | |
end | |
end | |
end | |
__END__ | |
# Here's other random stuff | |
base_path = "/Users/pumpkin/Desktop/iPhone1,2_2.0_5A347_Restore" | |
if devs[0].kind_of?(RecoveryV2Mode) | |
x = devs[0].open | |
p "sending apple logo" | |
File.open("#{base_path}/Firmware/all_flash/all_flash.n82ap.production/applelogo.s5l8900x.img3") do |f| | |
x.send_file(f) | |
end | |
x.send_command("setpicture 1") | |
x.send_command("bgcolor 0 255 0") | |
p "sending device tree" | |
File.open("#{base_path}/Firmware/all_flash/all_flash.n82ap.production/DeviceTree.n82ap.img3") do |f| | |
x.send_file(f) | |
end | |
x.send_command("devicetree") | |
pp x.recv_buffer.split("\x00") | |
p "sending ramdisk" | |
File.open("#{base_path}/018-3783-2.dmg") do |f| | |
x.send_file(f) | |
end | |
x.send_command("ramdisk") | |
pp x.recv_buffer.split("\x00") | |
p "sending kernel" | |
File.open("#{base_path}/kernelcache.release.s5l8900x") do |f| | |
x.send_file(f) | |
end | |
p "booting" | |
x.send_command("bootx") | |
pp x.recv_buffer.split("\x00") | |
p "sleeping" | |
#sleep(60) | |
end | |
devs = AppleDevice.available_devices | |
p devs[0] | |
if devs[0].kind_of?(NormalMode) | |
restore = devs[0].open | |
progress_callback = proc do |operation, progress| | |
steps = { | |
28 => "Waiting for NAND", | |
11 => "Creating partition map", | |
12 => "Creating filesystem", | |
13 => "Restoring image", | |
14 => "Verifying restore", | |
15 => "Checking filesystems", | |
16 => "Mounting filesystems", | |
29 => "Fixing up /var", | |
29 => "Unmounting filesystems", | |
25 => "Modifying persistent boot-args", | |
35 => "Loading NOR data to flash", | |
18 => "Flashing NOR", | |
19 => "Updating baseband", | |
20 => "Finalizing NAND epoch update", | |
32 => "Waiting for Device..." | |
} | |
puts "#{steps[operation]} (#{operation}) with progress #{progress}" | |
end | |
p "starting restore" | |
restore.start_restore(progress_callback) do |data_type| | |
puts "DataRequest callback" | |
if data_type == "SystemImageData" | |
puts "Got request for system image data" | |
Thread.new do | |
puts "Started ASR thread" | |
File.open("#{base_path}/018-3782-2.dmg") do |f| | |
asr = ASRService.new(12345, f) | |
asr.start | |
end | |
end | |
nil | |
elsif data_type == "NORData" | |
puts "Got request for NOR data" | |
llb_data = File.read("#{base_path}/Firmware/all_flash/all_flash.n82ap.production/LLB.n82ap.RELEASE.img3") | |
other_nor_data = Dir["#{base_path}/Firmware/all_flash/all_flash.n82ap.production/*.img3"].reject{|x| x =~ /^LLB/}.map do |path| | |
buffer = File.read(path) | |
OSX::NSData.alloc.initWithBytes_length(buffer, buffer.size) | |
end | |
response = { "LlbImageData" => OSX::NSData.alloc.initWithBytes_length(llb_data, llb_data.size), | |
"NorImageData" => other_nor_data } | |
response | |
end | |
end | |
puts "Rebooting" | |
end | |
__END__ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment