From 34463536aad247426880b76e153b800187ff65af Mon Sep 17 00:00:00 2001 From: David Missmann Date: Wed, 18 Oct 2023 10:33:00 +0200 Subject: [PATCH 1/2] Encode and decode remote XPC messages Not all types and flags are implemented yet. It works only with maps and not custom struct types --- ios/xpc/encoding.go | 497 +++++++++++++++++++++++++++++++++++++ ios/xpc/encoding_test.go | 135 ++++++++++ ios/xpc/xpc_dict.bin | Bin 0 -> 1076 bytes ios/xpc/xpc_empty_dict.bin | Bin 0 -> 44 bytes 4 files changed, 632 insertions(+) create mode 100644 ios/xpc/encoding.go create mode 100644 ios/xpc/encoding_test.go create mode 100644 ios/xpc/xpc_dict.bin create mode 100644 ios/xpc/xpc_empty_dict.bin diff --git a/ios/xpc/encoding.go b/ios/xpc/encoding.go new file mode 100644 index 00000000..791fb7e5 --- /dev/null +++ b/ios/xpc/encoding.go @@ -0,0 +1,497 @@ +package xpc + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "math" + "reflect" + "strings" +) + +const bodyVersion = uint32(0x00000005) + +const ( + wrapperMagic = uint32(0x29b00b92) + objectMagic = uint32(0x42133742) +) + +type xpcType uint32 + +// TODO: there are more types available and need to be added still when observed +const ( + nullType = xpcType(0x00001000) + boolType = xpcType(0x00002000) + int64Type = xpcType(0x00003000) + uint64Type = xpcType(0x00004000) + dataType = xpcType(0x00008000) + stringType = xpcType(0x00009000) + arrayType = xpcType(0x0000e000) + dictionaryType = xpcType(0x0000f000) +) + +const ( + alwaysSetFlag = uint32(0x00000001) + dataFlag = uint32(0x00000100) + heartbeatRequestFlag = uint32(0x00010000) + heartbeatReplyFlag = uint32(0x00020000) +) + +type wrapperHeader struct { + Flags uint32 + BodyLen uint64 + MsgId uint64 +} + +type Message struct { + Flags uint32 + Body map[string]interface{} +} + +// DecodeMessage expects a full RemoteXPC message and decodes the message body into a map +func DecodeMessage(r io.Reader) (Message, error) { + var magic uint32 + if err := binary.Read(r, binary.LittleEndian, &magic); err != nil { + return Message{}, err + } + if magic != wrapperMagic { + return Message{}, fmt.Errorf("wrong magic number") + } + wrapper, err := decodeWrapper(r) + return wrapper, err +} + +// EncodeData creates a RemoteXPC message with the data flag set, if data is present (an empty dictionary is considered +// to be no data) +func EncodeData(w io.Writer, body map[string]interface{}) error { + buf := bytes.NewBuffer(nil) + err := encodeDictionary(buf, body) + if err != nil { + return err + } + + flags := alwaysSetFlag + if len(body) > 0 { + flags |= dataFlag + } + + wrapper := struct { + magic uint32 + h wrapperHeader + body struct { + magic uint32 + version uint32 + } + }{ + magic: wrapperMagic, + h: wrapperHeader{ + Flags: flags, + BodyLen: uint64(buf.Len() + 8), + MsgId: 0, + }, + body: struct { + magic uint32 + version uint32 + }{ + magic: objectMagic, + version: bodyVersion, + }, + } + + err = binary.Write(w, binary.LittleEndian, wrapper) + if err != nil { + return err + } + + _, err = io.Copy(w, buf) + return err +} + +func decodeWrapper(r io.Reader) (Message, error) { + var h wrapperHeader + err := binary.Read(r, binary.LittleEndian, &h) + if err != nil { + return Message{}, err + } + body, err := decodeBody(r, h) + return Message{ + Flags: h.Flags, + Body: body, + }, err +} + +func decodeBody(r io.Reader, h wrapperHeader) (map[string]interface{}, error) { + bodyHeader := struct { + Magic uint32 + Version uint32 + }{} + if err := binary.Read(r, binary.LittleEndian, &bodyHeader); err != nil { + return nil, err + } + if bodyHeader.Magic != objectMagic { + return nil, fmt.Errorf("cant decode") + } + if bodyHeader.Version != bodyVersion { + return nil, fmt.Errorf("expected version 0x%x but got 0x%x", bodyVersion, bodyHeader.Version) + } + body := make([]byte, h.BodyLen-8) + n, err := r.Read(body) + if err != nil { + return nil, err + } + if uint64(n) != (h.BodyLen - 8) { + return nil, fmt.Errorf("not enough data") + } + bodyBuf := bytes.NewReader(body) + res, err := decodeObject(bodyBuf) + if err != nil { + return nil, err + } + return res.(map[string]interface{}), nil +} + +func decodeObject(r io.Reader) (interface{}, error) { + var t xpcType + err := binary.Read(r, binary.LittleEndian, &t) + if err != nil { + return nil, err + } + switch t { + case nullType: + return nil, nil + case boolType: + return decodeBool(r) + case int64Type: + return decodeInt64(r) + case uint64Type: + return decodeUint64(r) + case dataType: + return decodeData(r) + case stringType: + return decodeString(r) + case arrayType: + return decodeArray(r) + case dictionaryType: + return decodeDictionary(r) + default: + return nil, fmt.Errorf("can't handle unknown type 0x%08x", t) + } +} + +func decodeDictionary(r io.Reader) (map[string]interface{}, error) { + var l, numEntries uint32 + err := binary.Read(r, binary.LittleEndian, &l) + if err != nil { + return nil, err + } + err = binary.Read(r, binary.LittleEndian, &numEntries) + if err != nil { + return nil, err + } + dict := make(map[string]interface{}) + for i := uint32(0); i < numEntries; i++ { + key, err := readDictionaryKey(r) + if err != nil { + return nil, err + } + dict[key], err = decodeObject(r) + if err != nil { + return nil, err + } + } + return dict, nil +} + +func readDictionaryKey(r io.Reader) (string, error) { + var b strings.Builder + buf := make([]byte, 1) + for { + _, err := r.Read(buf) + if err != nil { + return "", err + } + if buf[0] == 0 { + s := b.String() + toSkip := calcPadding(len(s) + 1) + _, err := io.CopyN(io.Discard, r, toSkip) + return s, err + } + b.Write(buf) + } +} + +func decodeArray(r io.Reader) ([]interface{}, error) { + var l, numEntries uint32 + err := binary.Read(r, binary.LittleEndian, &l) + if err != nil { + return nil, err + } + err = binary.Read(r, binary.LittleEndian, &numEntries) + if err != nil { + return nil, err + } + arr := make([]interface{}, numEntries) + for i := uint32(0); i < numEntries; i++ { + arr[i], err = decodeObject(r) + if err != nil { + return nil, err + } + } + return arr, nil +} + +func decodeString(r io.Reader) (string, error) { + var l uint32 + err := binary.Read(r, binary.LittleEndian, &l) + if err != nil { + return "", err + } + s := make([]byte, l) + _, err = r.Read(s) + if err != nil { + return "", err + } + res := strings.Trim(string(s), "\000") + toSkip := calcPadding(int(l)) + _, _ = io.CopyN(io.Discard, r, toSkip) + return res, nil +} + +func decodeData(r io.Reader) ([]byte, error) { + var l uint32 + err := binary.Read(r, binary.LittleEndian, &l) + if err != nil { + return nil, err + } + b := make([]byte, l) + _, err = r.Read(b) + if err != nil { + return nil, err + } + toSkip := calcPadding(int(l)) + _, _ = io.CopyN(io.Discard, r, toSkip) + return b, nil +} + +func decodeUint64(r io.Reader) (uint64, error) { + var i uint64 + err := binary.Read(r, binary.LittleEndian, &i) + return i, err +} + +func decodeInt64(r io.Reader) (int64, error) { + var i int64 + err := binary.Read(r, binary.LittleEndian, &i) + if err != nil { + return 0, err + } + return i, nil +} + +func decodeBool(r io.Reader) (bool, error) { + var b bool + err := binary.Read(r, binary.LittleEndian, &b) + if err != nil { + return false, err + } + _, _ = io.CopyN(io.Discard, r, 3) + return b, nil +} + +func calcPadding(l int) int64 { + c := int(math.Ceil(float64(l) / 4.0)) + return int64(c*4 - l) +} + +func encodeDictionary(w io.Writer, v map[string]interface{}) error { + buf := bytes.NewBuffer(nil) + + for k, e := range v { + err := encodeDictionaryKey(buf, k) + if err != nil { + return err + } + err2 := encodeObject(buf, e) + if err2 != nil { + return err2 + } + } + + err := binary.Write(w, binary.LittleEndian, dictionaryType) + if err != nil { + return err + } + err = binary.Write(w, binary.LittleEndian, uint32(buf.Len())) + if err != nil { + return err + } + err = binary.Write(w, binary.LittleEndian, uint32(len(v))) + if err != nil { + return err + } + _, err = w.Write(buf.Bytes()) + return err +} + +func encodeObject(w io.Writer, e interface{}) error { + if e == nil { + if err := binary.Write(w, binary.LittleEndian, nullType); err != nil { + return err + } + return nil + } + if v := reflect.ValueOf(e); v.Kind() == reflect.Slice { + if b, ok := e.([]byte); ok { + return encodeData(w, b) + } + r := make([]interface{}, v.Len()) + for i := 0; i < v.Len(); i++ { + r[i] = v.Index(i).Interface() + } + if err := encodeArray(w, r); err != nil { + return err + } + return nil + } + switch t := e.(type) { + case bool: + if err := encodeBool(w, e.(bool)); err != nil { + return err + } + case int64: + if err := encodeInt64(w, e.(int64)); err != nil { + return err + } + case uint64: + if err := encodeUint64(w, e.(uint64)); err != nil { + return err + } + case string: + if err := encodeString(w, e.(string)); err != nil { + return err + } + case map[string]interface{}: + if err := encodeDictionary(w, e.(map[string]interface{})); err != nil { + return err + } + default: + return fmt.Errorf("can not encode type %v", t) + } + return nil +} + +func encodeArray(w io.Writer, slice []interface{}) error { + buf := bytes.NewBuffer(nil) + for _, e := range slice { + if err := encodeObject(buf, e); err != nil { + return err + } + } + + header := struct { + t xpcType + l uint32 + numObjects uint32 + }{arrayType, uint32(buf.Len()), uint32(len(slice))} + if err := binary.Write(w, binary.LittleEndian, header); err != nil { + return err + } + if _, err := io.Copy(w, buf); err != nil { + return err + } + return nil +} + +func encodeString(w io.Writer, s string) error { + header := struct { + t xpcType + l uint32 + }{stringType, uint32(len(s) + 1)} + err := binary.Write(w, binary.LittleEndian, header) + if err != nil { + return err + } + _, err = w.Write([]byte(s)) + if err != nil { + return err + } + toPad := calcPadding(int(header.l)) + _, err = w.Write(make([]byte, toPad+1)) + if err != nil { + return err + } + return nil +} + +func encodeData(w io.Writer, b []byte) error { + header := struct { + t xpcType + l uint32 + }{dataType, uint32(len(b))} + err := binary.Write(w, binary.LittleEndian, header) + if err != nil { + return err + } + _, err = w.Write(b) + if err != nil { + return err + } + toPad := calcPadding(int(header.l)) + _, err = w.Write(make([]byte, toPad)) + if err != nil { + return err + } + return nil +} + +func encodeUint64(w io.Writer, i uint64) error { + out := struct { + t xpcType + i uint64 + }{uint64Type, i} + err := binary.Write(w, binary.LittleEndian, out) + if err != nil { + return err + } + return nil +} + +func encodeInt64(w io.Writer, i int64) error { + out := struct { + t xpcType + i int64 + }{int64Type, i} + err := binary.Write(w, binary.LittleEndian, out) + if err != nil { + return err + } + return nil +} + +func encodeBool(w io.Writer, b bool) error { + out := struct { + t xpcType + b bool + pad [3]byte + }{ + t: boolType, + b: b, + } + err := binary.Write(w, binary.LittleEndian, out) + if err != nil { + return err + } + return nil +} + +func encodeDictionaryKey(w io.Writer, k string) error { + toPad := calcPadding(len(k) + 1) + _, err := w.Write(append([]byte(k), 0x0)) + if err != nil { + return err + } + pad := make([]byte, toPad) + _, err = w.Write(pad) + return err +} diff --git a/ios/xpc/encoding_test.go b/ios/xpc/encoding_test.go new file mode 100644 index 00000000..b749dcfb --- /dev/null +++ b/ios/xpc/encoding_test.go @@ -0,0 +1,135 @@ +package xpc + +import ( + "bytes" + "encoding/base64" + "github.com/stretchr/testify/assert" + "os" + "path" + "testing" +) + +func TestEmptyDictionary(t *testing.T) { + b, _ := os.ReadFile(path.Join("xpc_empty_dict.bin")) + + res, err := DecodeMessage(bytes.NewReader(b)) + assert.NoError(t, err) + assert.Equal(t, Message{ + Flags: alwaysSetFlag, + Body: map[string]interface{}{}, + }, res) +} + +func TestDictionary(t *testing.T) { + b, _ := os.ReadFile(path.Join("xpc_dict.bin")) + + res, err := DecodeMessage(bytes.NewReader(b)) + assert.NoError(t, err) + assert.Equal(t, Message{ + Flags: alwaysSetFlag | dataFlag | heartbeatRequestFlag, + Body: map[string]interface{}{ + "CoreDevice.CoreDeviceDDIProtocolVersion": int64(0), + "CoreDevice.action": map[string]interface{}{}, + "CoreDevice.coreDeviceVersion": map[string]interface{}{ + "components": []interface{}{uint64(0x15c), uint64(0x1), uint64(0x0), uint64(0x0), uint64(0x0)}, + "originalComponentsCount": int64(2), + "stringValue": "348.1", + }, + "CoreDevice.deviceIdentifier": "A7DD28AC-2911-4549-811D-85917B9AC72F", + "CoreDevice.featureIdentifier": "com.apple.coredevice.feature.launchapplication", + "CoreDevice.input": map[string]interface{}{ + "applicationSpecifier": map[string]interface{}{ + "bundleIdentifier": map[string]interface{}{ + "_0": "xxx.xxxxxxxxx.xxxxxxxx", + }, + }, + "options": map[string]interface{}{ + "arguments": []interface{}{}, + "environmentVariables": map[string]interface{}{ + "TERM": "xterm-256color", + }, + "platformSpecificOptions": base64Decode("YnBsaXN0MDDQCAAAAAAAAAEBAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAJ"), + "standardIOUsesPseudoterminals": true, + "startStopped": false, + "terminateExisting": false, + "user": map[string]interface{}{ + "active": true, + }, + "workingDirectory": nil, + }, + "standardIOIdentifiers": map[string]interface{}{}, + }, + "CoreDevice.invocationIdentifier": "62419FC1-5ABF-4D96-BCA8-7A5F6F9A69EE", + }, + }, res) +} + +func base64Decode(s string) []byte { + dst := make([]byte, base64.StdEncoding.DecodedLen(len(s))) + _, err := base64.StdEncoding.Decode(dst, []byte(s)) + if err != nil { + panic(err) + } + return dst +} + +func TestEncodeDecode(t *testing.T) { + tests := []struct { + name string + input map[string]interface{} + expectedFlags uint32 + }{ + { + name: "empty dict", + input: map[string]interface{}{}, + expectedFlags: alwaysSetFlag, + }, + { + name: "keys without padding", + input: map[string]interface{}{ + "key": "value", + "key-key": "value", + }, + expectedFlags: alwaysSetFlag | dataFlag, + }, + { + name: "nested values", + input: map[string]interface{}{ + "key1": "string-val", + "nested-dict": map[string]interface{}{ + "bool": true, + "int64": int64(123), + "uint64": uint64(321), + "data": []byte{0x1}, + }, + }, + expectedFlags: alwaysSetFlag | dataFlag, + }, + { + name: "null entry", + input: map[string]interface{}{ + "null": nil, + }, + expectedFlags: alwaysSetFlag | dataFlag, + }, + { + name: "dictionary with array", + input: map[string]interface{}{ + "array": []interface{}{uint64(1), uint64(2), uint64(3)}, + }, + expectedFlags: alwaysSetFlag | dataFlag, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + err := EncodeData(buf, tt.input) + assert.NoError(t, err) + res, err := DecodeMessage(buf) + assert.NoError(t, err) + assert.Equal(t, tt.input, res.Body) + assert.Equal(t, tt.expectedFlags, res.Flags) + }) + } +} diff --git a/ios/xpc/xpc_dict.bin b/ios/xpc/xpc_dict.bin new file mode 100644 index 0000000000000000000000000000000000000000..e49919f287891e737909b22587e6ed91b29825a5 GIT binary patch literal 1076 zcmZuwOODe(5Uub>Ab|uD8y2$w36N~!ke_$s*onvj&4_`lkWk0&nQ7s4TkUoTJB~17 z#W6SvN0<7?!kQ!AQ`9!#!V`!~^DU^ijL zO7n!TMaunjZ!Wklg_14|-0v{1;!#?h3CZ$UtqLW%G-a$RX^6T+R~(rr-D7ZE5*8iBnt9RVCChg)q`%Px%;@@&EKiW6NB-2nWmKlTIYR{xnh#ymq~;#6?$xKF`} zMoH2eN3qungTU(#`k^-tg2WpS!eBHBqj=Ptf!5B|q)n3fu~tT#U$|E0$k+GoGQIKDR_#>q(?i>SE3idmqjj0)9wDWWC*TKEjzva zq}xQLVcMJ+RTMmP?t>cqtn?==DG{2dM(Hokd!NW~y~3R_e7ZpmFf4H3P@79;OlR|> z_a!fnOI~Ga%g=EF$FnU|rClZjb9e}4eReo&TnC%Wesj4NTFDibzhGLhWzJm+8}kIX z<-D7|eFNq0#XYEvE#~zGL+n?Mlxv&!}NZ Date: Mon, 23 Oct 2023 12:40:19 +0200 Subject: [PATCH 2/2] handle messages without a body --- ios/xpc/encoding.go | 24 ++++++++++++++++++++++++ ios/xpc/encoding_test.go | 5 +++++ 2 files changed, 29 insertions(+) diff --git a/ios/xpc/encoding.go b/ios/xpc/encoding.go index 791fb7e5..b3d9b134 100644 --- a/ios/xpc/encoding.go +++ b/ios/xpc/encoding.go @@ -65,6 +65,9 @@ func DecodeMessage(r io.Reader) (Message, error) { // EncodeData creates a RemoteXPC message with the data flag set, if data is present (an empty dictionary is considered // to be no data) func EncodeData(w io.Writer, body map[string]interface{}) error { + if body == nil { + return encodeMessageWithoutBody(w) + } buf := bytes.NewBuffer(nil) err := encodeDictionary(buf, body) if err != nil { @@ -114,6 +117,11 @@ func decodeWrapper(r io.Reader) (Message, error) { if err != nil { return Message{}, err } + if h.BodyLen == 0 { + return Message{ + Flags: h.Flags, + }, nil + } body, err := decodeBody(r, h) return Message{ Flags: h.Flags, @@ -495,3 +503,19 @@ func encodeDictionaryKey(w io.Writer, k string) error { _, err = w.Write(pad) return err } + +func encodeMessageWithoutBody(w io.Writer) error { + wrapper := struct { + magic uint32 + h wrapperHeader + }{ + magic: wrapperMagic, + h: wrapperHeader{ + Flags: alwaysSetFlag, + BodyLen: 0, + MsgId: 0, + }, + } + err := binary.Write(w, binary.LittleEndian, wrapper) + return err +} diff --git a/ios/xpc/encoding_test.go b/ios/xpc/encoding_test.go index b749dcfb..63b201dd 100644 --- a/ios/xpc/encoding_test.go +++ b/ios/xpc/encoding_test.go @@ -84,6 +84,11 @@ func TestEncodeDecode(t *testing.T) { input: map[string]interface{}{}, expectedFlags: alwaysSetFlag, }, + { + name: "no xpc body", + input: nil, + expectedFlags: alwaysSetFlag, + }, { name: "keys without padding", input: map[string]interface{}{