├── .github ├── actions │ └── install-frida-devkit │ │ └── action.yml ├── dependabot.yml └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── CODEOWNERS ├── LICENSE ├── README.md ├── airplane_mode.png ├── go.mod ├── go.sum ├── logger.go ├── main.go ├── object.go ├── offsets.go ├── package-lock.json ├── package.json ├── running.png ├── running_one.png └── script.ts /.github/actions/install-frida-devkit/action.yml: -------------------------------------------------------------------------------- 1 | name: Install Frida Devkit 2 | description: Install Frida Devkit 3 | inputs: 4 | arch: 5 | required: true 6 | path: the architecture of the devkit 7 | os: 8 | required: true 9 | path: the target operating system of the devkit 10 | version: 11 | required: true 12 | path: the version of the devkit 13 | outdir: 14 | required: true 15 | path: where to save header and dylib 16 | runs: 17 | using: composite 18 | steps: 19 | - run: | 20 | mkdir /tmp/frida-core-devkit && cd /tmp/frida-core-devkit 21 | wget https://github.com/frida/frida/releases/download/${{ inputs.version }}/frida-core-devkit-${{ inputs.version }}-${{ inputs.os }}-${{ inputs.arch }}.tar.xz -O - | tar --extract --xz 22 | mkdir -p ${{ inputs.outdir }}/include 23 | mkdir -p ${{ inputs.outdir }}/lib 24 | cp frida-core.h ${{ inputs.outdir }}/include 25 | cp libfrida-core.* ${{ inputs.outdir }}/lib 26 | rm -rf /tmp/frida-core-devkit 27 | shell: bash 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - v*.*.* 6 | 7 | jobs: 8 | release: 9 | strategy: 10 | matrix: 11 | frida_version: ["17.1.3"] 12 | runs-on: macos-latest 13 | steps: 14 | - 15 | name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - 20 | name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: '^1.22.x' 24 | - run: go version 25 | - 26 | name: Fetch all tags 27 | run: git fetch --force --tags 28 | - 29 | name: Download Frida macOS_amd64 30 | uses: ./.github/actions/install-frida-devkit 31 | with: 32 | arch: x86_64 33 | os: macos 34 | version: ${{ matrix.frida_version }} 35 | outdir: /tmp/data/macos_amd64 36 | - 37 | name: Download Frida macOS_arm64 38 | uses: ./.github/actions/install-frida-devkit 39 | with: 40 | arch: arm64 41 | os: macos 42 | version: ${{ matrix.frida_version }} 43 | outdir: /tmp/data/macos_arm64 44 | - 45 | name: Run GoReleaser 46 | uses: goreleaser/goreleaser-action@v4 47 | with: 48 | distribution: goreleaser 49 | version: latest 50 | args: release --clean 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/go,macos,git 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,macos,git 3 | 4 | ### Git ### 5 | # Created by git for backups. To disable backups in Git: 6 | # $ git config --global mergetool.keepBackup false 7 | *.orig 8 | 9 | # Created by git when using merge tools for conflicts 10 | *.BACKUP.* 11 | *.BASE.* 12 | *.LOCAL.* 13 | *.REMOTE.* 14 | *_BACKUP_*.txt 15 | *_BASE_*.txt 16 | *_LOCAL_*.txt 17 | *_REMOTE_*.txt 18 | 19 | ### Go ### 20 | # If you prefer the allow list template instead of the deny list, see community template: 21 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 22 | # 23 | # Binaries for programs and plugins 24 | *.exe 25 | *.exe~ 26 | *.dll 27 | *.so 28 | *.dylib 29 | 30 | # Test binary, built with `go test -c` 31 | *.test 32 | 33 | # Output of the go coverage tool, specifically when used with LiteIDE 34 | *.out 35 | 36 | # Dependency directories (remove the comment below to include it) 37 | # vendor/ 38 | 39 | # Go workspace file 40 | go.work 41 | 42 | ### macOS ### 43 | # General 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | 48 | # Icon must end with two \r 49 | Icon 50 | 51 | # Thumbnails 52 | ._* 53 | 54 | # Files that might appear in the root of a volume 55 | .DocumentRevisions-V100 56 | .fseventsd 57 | .Spotlight-V100 58 | .TemporaryItems 59 | .Trashes 60 | .VolumeIcon.icns 61 | .com.apple.timemachine.donotpresent 62 | 63 | # Directories potentially created on remote AFP share 64 | .AppleDB 65 | .AppleDesktop 66 | Network Trash Folder 67 | Temporary Items 68 | .apdisk 69 | 70 | ### macOS Patch ### 71 | # iCloud generated files 72 | *.icloud 73 | 74 | # End of https://www.toptal.com/developers/gitignore/api/go,macos,git 75 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - id: darwin_amd64 6 | binary: gxpc 7 | env: 8 | - CGO_ENABLED=1 9 | - CGO_LDFLAGS=-L/tmp/data/macos_amd64/lib 10 | - CGO_CFLAGS=-I/tmp/data/macos_amd64/include 11 | goos: 12 | - darwin 13 | goarch: 14 | - amd64 15 | flags: 16 | - -trimpath 17 | ldflags: -s -w -X main.Version={{.Tag}} 18 | - id: darwin_arm64 19 | binary: gxpc 20 | env: 21 | - CGO_ENABLED=1 22 | - CGO_LDFLAGS=-L/tmp/data/macos_arm64/lib 23 | - CGO_CFLAGS=-I/tmp/data/macos_arm64/include 24 | goos: 25 | - darwin 26 | goarch: 27 | - arm64 28 | flags: 29 | - -trimpath 30 | ldflags: -s -w -X main.Version={{.Tag}} 31 | checksum: 32 | name_template: 'checksums.txt' 33 | snapshot: 34 | name_template: "{{ incpatch .Version }}-next" 35 | changelog: 36 | sort: asc 37 | filters: 38 | exclude: 39 | - '^docs:' 40 | - '^test:' 41 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Global 2 | * @NSEcho 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ReverseApple 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gxpc 2 | 3 | Tool inspired by [xpcspy](https://github.com/hot3eed/xpcspy) tool to monitor XPC traffic. 4 | 5 | gxpc recursively parses types of `xpc_object_t` as well as unmarshalling the data back to Go types. 6 | 7 | # Installation 8 | 9 | Download one of the prebuilt binaries for macOS(x86_64 or arm64) from [here](https://github.com/ReverseApple/gxpc/releases) 10 | or do it manually as described below. 11 | 12 | * Follow the instructions for devkit documented [here](https://github.com/frida/frida-go) 13 | * Run `go install github.com/ReverseApple/gxpc@latest` 14 | 15 | # Usage 16 | 17 | ```bash 18 | XPC sniffer 19 | 20 | Usage: 21 | gxpc [spawn_args] [flags] 22 | 23 | Flags: 24 | -b, --blacklist strings blacklist connection by name 25 | --blacklistp strings blacklist connection by PID 26 | -f, --file string spawn the file 27 | -h, --help help for gxpc 28 | -i, --id string connect to device with ID 29 | -l, --list list available devices 30 | -n, --name string process name 31 | -o, --output string save output to this file 32 | -p, --pid int PID of wanted process (default -1) 33 | -r, --remote string connect to device at IP address 34 | -w, --whitelist strings whitelist connection by name 35 | --whitelistp strings whitelist connection by PID 36 | ``` 37 | 38 | If you do not pass `-i` flag, default device is USB. 39 | 40 | If you want to spawn a file/binary, pass the `-f` that points to the file/binary you want to spawn along with the arguments. 41 | 42 | * `gxpc -i local -f /bin/cat /tmp/somefile` - without some specific flags to the spawned binary 43 | * `gxpc -i local -f /path/to/binary -- -a -b "TEST"` - with some specific flags to the spawned binary 44 | 45 | Additionally, you can filter on connection names or PIDs, if you pass both filters (name and PID), it will take only name. 46 | `--whitelist` and `--whitelistp` along with the `--blacklist` and `--blacklistp` accepts the list, such as `--blacklistp "89,32,41"` which will 47 | blacklist the ports 89, 32 and 41. 48 | 49 | ![Running gxpc](running.png) 50 | 51 | ![Running against Signal](running_one.png) 52 | -------------------------------------------------------------------------------- /airplane_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReverseApple/gxpc/67b3e575b5d8a1fe9115cfd39e0affbf03d477b7/airplane_mode.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ReverseApple/gxpc 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/fatih/color v1.18.0 7 | github.com/frida/frida-go v1.0.0 8 | github.com/spf13/cobra v1.9.1 9 | ) 10 | 11 | require ( 12 | github.com/google/uuid v1.6.0 // indirect 13 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 14 | github.com/mattn/go-colorable v0.1.13 // indirect 15 | github.com/mattn/go-isatty v0.0.20 // indirect 16 | github.com/spf13/pflag v1.0.6 // indirect 17 | golang.org/x/sys v0.25.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 3 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 4 | github.com/frida/frida-go v1.0.0 h1:Xlq1CB8QSAC6zbOFdjCX0oK8RjQGtdl2yATbUQKROwo= 5 | github.com/frida/frida-go v1.0.0/go.mod h1:O8Dg1YBGfQsBEL1a8x3GURw/JllJrcuvg78ga2OgdM4= 6 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 7 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 8 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 9 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 10 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 11 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 12 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 13 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 14 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 15 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 16 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 17 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 18 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 19 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 20 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 23 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fatih/color" 6 | "log" 7 | "os" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | var ( 13 | infoColor = color.New(color.FgGreen) 14 | warnColor = color.New(color.FgYellow) 15 | errorColor = color.New(color.FgRed) 16 | fatalColor = color.New(color.FgHiRed) 17 | scriptColor = color.New(color.FgMagenta) 18 | ) 19 | 20 | type Logger struct { 21 | infoLogger *log.Logger 22 | warnLogger *log.Logger 23 | errorLogger *log.Logger 24 | fatalLogger *log.Logger 25 | scriptLogger *log.Logger 26 | f *os.File 27 | } 28 | 29 | func NewLogger() *Logger { 30 | return &Logger{ 31 | infoLogger: log.New(os.Stdout, infoColor.Sprintf("%s ", "⚡"), log.Ldate|log.Ltime), 32 | warnLogger: log.New(os.Stdout, warnColor.Sprintf("%s ", "⚠"), log.Ldate|log.Ltime), 33 | errorLogger: log.New(os.Stderr, errorColor.Sprintf("%s ", "❗️"), log.Ldate|log.Ltime), 34 | fatalLogger: log.New(os.Stderr, fatalColor.Sprintf("%s ", "⛔️"), log.Ldate|log.Ltime), 35 | scriptLogger: log.New(os.Stdout, scriptColor.Sprintf("%s ", "✅"), 0), 36 | } 37 | } 38 | 39 | func (l *Logger) SetOutput(output string) error { 40 | f, err := os.OpenFile(output, os.O_RDWR|os.O_CREATE|os.O_APPEND, os.ModePerm) 41 | if err != nil { 42 | return err 43 | } 44 | l.f = f 45 | return nil 46 | } 47 | 48 | func (l *Logger) Close() error { 49 | if l.f != nil { 50 | return l.f.Close() 51 | } 52 | return nil 53 | } 54 | 55 | func (l *Logger) Infof(format string, args ...any) { 56 | l.infoLogger.Printf(format, args...) 57 | l.writeToFile("INFO", format, args...) 58 | } 59 | 60 | func (l *Logger) Warnf(format string, args ...any) { 61 | l.warnLogger.Printf(format, args...) 62 | l.writeToFile("WARN", format, args...) 63 | } 64 | 65 | func (l *Logger) Errorf(format string, args ...any) { 66 | l.errorLogger.Printf(format, args...) 67 | l.writeToFile("ERRO", format, args...) 68 | } 69 | 70 | func (l *Logger) Fatalf(format string, args ...any) { 71 | l.fatalLogger.Printf(format, args...) 72 | l.writeToFile("FATA", format, args...) 73 | os.Exit(1) 74 | } 75 | 76 | func (l *Logger) Scriptf(format string, args ...any) { 77 | l.scriptLogger.Printf(format, args...) 78 | } 79 | 80 | func (l *Logger) writeToFile(level, format string, args ...any) { 81 | if l.f != nil { 82 | t := time.Now().Format(time.RFC3339) 83 | msg := fmt.Sprintf("%s\n%s: %s\n%s\n", 84 | t, level, fmt.Sprintf(format, args...), strings.Repeat("=", 80)) 85 | l.f.WriteString(msg) 86 | } 87 | } 88 | 89 | func (l *Logger) writeToFileScript(body string) { 90 | if l.f != nil { 91 | t := time.Now().Format(time.RFC3339) 92 | l.f.WriteString(t + "\nSCRI: " + body) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "github.com/frida/frida-go/frida" 10 | "github.com/spf13/cobra" 11 | "os" 12 | "os/exec" 13 | "os/signal" 14 | "path/filepath" 15 | "regexp" 16 | "strings" 17 | "syscall" 18 | ) 19 | 20 | const ( 21 | agentFilename = "_agent.js" 22 | ) 23 | 24 | //go:embed script.ts 25 | var scriptContent []byte 26 | 27 | //go:embed package.json 28 | var packageJSON []byte 29 | 30 | //go:embed package-lock.json 31 | var packageLockJSON []byte 32 | 33 | var tempFiles = map[string][]byte{ 34 | "script.ts": scriptContent, 35 | "package.json": packageJSON, 36 | "package-lock.json": packageLockJSON, 37 | } 38 | 39 | var Version string 40 | 41 | var logger *Logger = nil 42 | 43 | var rootCmd = &cobra.Command{ 44 | Use: "gxpc [spawn_args]", 45 | Short: "XPC sniffer", 46 | Version: Version, 47 | SilenceErrors: true, 48 | SilenceUsage: true, 49 | RunE: func(cmd *cobra.Command, args []string) error { 50 | config, err := cmd.Flags().GetString("config") 51 | if err != nil { 52 | return err 53 | } 54 | 55 | if config == "" { 56 | home, _ := os.UserHomeDir() 57 | config = filepath.Join(home, "gxpc.conf") 58 | } 59 | 60 | list, err := cmd.Flags().GetBool("list") 61 | if err != nil { 62 | return err 63 | } 64 | 65 | id, err := cmd.Flags().GetString("id") 66 | if err != nil { 67 | return err 68 | } 69 | 70 | remote, err := cmd.Flags().GetString("remote") 71 | if err != nil { 72 | return err 73 | } 74 | 75 | pid, err := cmd.Flags().GetInt("pid") 76 | if err != nil { 77 | return err 78 | } 79 | 80 | procName, err := cmd.Flags().GetString("name") 81 | if err != nil { 82 | return err 83 | } 84 | 85 | output, err := cmd.Flags().GetString("output") 86 | if err != nil { 87 | return err 88 | } 89 | 90 | if output != "" { 91 | if err := logger.SetOutput(output); err != nil { 92 | return err 93 | } 94 | } 95 | 96 | mgr := frida.NewDeviceManager() 97 | devices, err := mgr.EnumerateDevices() 98 | if err != nil { 99 | return err 100 | } 101 | 102 | if list { 103 | for _, d := range devices { 104 | logger.Infof("[%s]\t%s (%s)\n", 105 | strings.ToUpper(d.DeviceType().String()), 106 | d.Name(), 107 | d.ID()) 108 | } 109 | return nil 110 | } 111 | 112 | var dev *frida.Device 113 | var session *frida.Session 114 | 115 | for _, d := range devices { 116 | if id != "" { 117 | if d.ID() == id { 118 | dev = d.(*frida.Device) 119 | break 120 | } 121 | } else if remote != "" { 122 | rdevice, err := mgr.AddRemoteDevice(remote, nil) 123 | if err != nil { 124 | return err 125 | } 126 | dev = rdevice.(*frida.Device) 127 | break 128 | } else { 129 | dev = frida.USBDevice() 130 | } 131 | } 132 | 133 | if dev == nil { 134 | return errors.New("could not obtain specified device") 135 | } 136 | defer dev.Clean() 137 | logger.Infof("Using device %s (%s)", dev.Name(), dev.ID()) 138 | 139 | procPid := pid 140 | 141 | if pid == -1 && procName != "" { 142 | processes, err := dev.EnumerateProcesses(frida.ScopeMinimal) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | for _, proc := range processes { 148 | if proc.Name() == procName { 149 | procPid = proc.PID() 150 | break 151 | } 152 | } 153 | } 154 | 155 | file, err := cmd.Flags().GetString("file") 156 | if err != nil { 157 | return err 158 | } 159 | 160 | if procPid == -1 && file == "" { 161 | return errors.New("missing pid, name or file to spawn") 162 | } 163 | 164 | spawned := false 165 | 166 | if procPid != -1 { 167 | session, err = dev.Attach(procPid, nil) 168 | if err != nil { 169 | return err 170 | } 171 | } else { 172 | opts := frida.NewSpawnOptions() 173 | argv := make([]string, len(args)+1) 174 | argv[0] = file 175 | for i, arg := range args { 176 | argv[i+1] = arg 177 | } 178 | opts.SetArgv(argv) 179 | spawnedPID, err := dev.Spawn(file, opts) 180 | if err != nil { 181 | return err 182 | } 183 | procPid = spawnedPID 184 | session, err = dev.Attach(spawnedPID, nil) 185 | if err != nil { 186 | return err 187 | } 188 | spawned = true 189 | } 190 | defer session.Clean() 191 | 192 | logger.Infof("Attached to the process with PID => %d", procPid) 193 | 194 | detached := make(chan struct{}) 195 | 196 | session.On("detached", func(reason frida.SessionDetachReason, crash *frida.Crash) { 197 | logger.Errorf("Session detached: %s: %v", reason.String(), crash) 198 | if crash != nil { 199 | logger.Errorf("Crash: %s %s", crash.Report(), crash.Summary()) 200 | } 201 | detached <- struct{}{} 202 | }) 203 | 204 | // Create temp files 205 | tempDir := filepath.Join(os.TempDir(), "gxpc") 206 | 207 | os.MkdirAll(tempDir, os.ModePerm) 208 | 209 | if _, err = os.Stat(filepath.Join(tempDir, "script.ts")); os.IsNotExist(err) { 210 | for fl, data := range tempFiles { 211 | os.WriteFile(filepath.Join(tempDir, fl), data, os.ModePerm) 212 | } 213 | } 214 | 215 | if _, err = os.Stat(filepath.Join(tempDir, "node_modules")); os.IsNotExist(err) { 216 | // Install modules 217 | pwd, _ := os.Getwd() 218 | os.Chdir(tempDir) 219 | command := exec.Command("npm", "install") 220 | if err := command.Run(); err != nil { 221 | logger.Errorf("Error installing modules: %v", err) 222 | } 223 | os.Chdir(pwd) 224 | } 225 | 226 | agentPath := filepath.Join(tempDir, agentFilename) 227 | var scriptBody string 228 | 229 | // check if we have script.ts already compiled 230 | if _, err = os.Stat(agentPath); os.IsNotExist(err) { 231 | comp := frida.NewCompiler() 232 | 233 | comp.On("finished", func() { 234 | logger.Infof("Done compiling script") 235 | }) 236 | 237 | comp.On("diagnostics", func(diag string) { 238 | logger.Errorf("compilation error: %v", diag) 239 | }) 240 | 241 | buildOptions := frida.NewCompilerOptions() 242 | buildOptions.SetProjectRoot(tempDir) 243 | buildOptions.SetJSCompression(frida.JSCompressionTerser) 244 | buildOptions.SetSourceMaps(frida.SourceMapsOmitted) 245 | 246 | bundle, err := comp.Build("script.ts", buildOptions) 247 | if err != nil { 248 | return fmt.Errorf("error compiling script: %v", err) 249 | } 250 | 251 | if err := os.WriteFile(agentPath, []byte(bundle), os.ModePerm); err != nil { 252 | return fmt.Errorf("error saving agent script: %v", err) 253 | } 254 | 255 | scriptBody = bundle 256 | } else { 257 | data, err := os.ReadFile(agentPath) 258 | if err != nil { 259 | return fmt.Errorf("error reading agent script: %v", err) 260 | } 261 | scriptBody = string(data) 262 | } 263 | 264 | script, err := session.CreateScript(scriptBody) 265 | if err != nil { 266 | return err 267 | } 268 | defer script.Clean() 269 | 270 | blacklist, err := cmd.Flags().GetStringSlice("blacklist") 271 | if err != nil { 272 | return err 273 | } 274 | 275 | whitelist, err := cmd.Flags().GetStringSlice("whitelist") 276 | if err != nil { 277 | return err 278 | } 279 | 280 | blacklistp, err := cmd.Flags().GetStringSlice("blacklistp") 281 | if err != nil { 282 | return err 283 | } 284 | 285 | whitelistp, err := cmd.Flags().GetStringSlice("whitelist") 286 | if err != nil { 287 | return err 288 | } 289 | 290 | var offsets *OffsetsData = nil 291 | 292 | script.On("message", func(message string) { 293 | msg, _ := frida.ScriptMessageToMessage(message) 294 | switch msg.Type { 295 | case frida.MessageTypeSend: 296 | payload := msg.Payload.(map[string]any) 297 | 298 | subType := payload["type"].(string) 299 | subPayload := payload["payload"] 300 | 301 | switch subType { 302 | case "print": 303 | PrintData( 304 | subPayload, 305 | false, 306 | false, 307 | listToRegex(whitelist), 308 | listToRegex(blacklist), 309 | listToRegex(whitelistp), 310 | listToRegex(blacklistp), 311 | logger, 312 | ) 313 | 314 | case "jlutil": 315 | resPayload, err := jlutil(subPayload.(string)) 316 | if err != nil { 317 | logger.Errorf("jlutil: %v", err) 318 | } 319 | 320 | msg := fmt.Sprintf(`{"type":"jlutil","payload":"%s"}`, resPayload) 321 | script.Post(msg, nil) 322 | 323 | case "newOffset": 324 | var newOffset NewOffset 325 | for k, v := range payload { 326 | val := v.(string) 327 | switch k { 328 | case "callEvent": 329 | newOffset.CallEvent = val 330 | case "plistCreate": 331 | newOffset.PlistCreate = val 332 | case "machine": 333 | newOffset.Machine = val 334 | case "version": 335 | newOffset.Version = val 336 | } 337 | } 338 | updateConfig(config, &newOffset) 339 | logger.Infof("Saved offset for %s (%s)", newOffset.Machine, newOffset.Version) 340 | 341 | default: 342 | logger.Warnf("SCRIPT: %v", subPayload) 343 | } 344 | 345 | case frida.MessageTypeLog: 346 | logger.Infof("SCRIPT: %v", msg.Payload.(string)) 347 | default: 348 | logger.Errorf("SCRIPT: %v", msg) 349 | } 350 | }) 351 | 352 | if err := script.Load(); err != nil { 353 | return err 354 | } 355 | logger.Infof("Loaded script to the process") 356 | 357 | if spawned { 358 | if err := dev.Resume(procPid); err != nil { 359 | return err 360 | } else { 361 | logger.Infof("Resumed process") 362 | } 363 | } 364 | 365 | if _, err := os.Stat(config); os.IsNotExist(err) { 366 | _ = script.ExportsCall("setup", nil) 367 | } else { 368 | f, err := os.Open(config) 369 | if err != nil { 370 | return err 371 | } 372 | defer f.Close() 373 | offsets = &OffsetsData{} 374 | if err := json.NewDecoder(f).Decode(offsets); err != nil { 375 | return err 376 | } 377 | _ = script.ExportsCall("setup", offsets) 378 | } 379 | 380 | logger.Infof("Finished setup") 381 | 382 | c := make(chan os.Signal) 383 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 384 | 385 | select { 386 | case <-c: 387 | fmt.Println() 388 | logger.Infof("Exiting...") 389 | if err := script.Unload(); err != nil { 390 | return err 391 | } 392 | logger.Infof("Script unloaded") 393 | case <-detached: 394 | logger.Infof("Exiting...") 395 | } 396 | return nil 397 | }, 398 | } 399 | 400 | func jlutil(payload string) (string, error) { 401 | decodedPayload, err := base64.StdEncoding.DecodeString(payload) 402 | if err != nil { 403 | return "", err 404 | } 405 | 406 | f, err := os.CreateTemp(os.TempDir(), "") 407 | if err != nil { 408 | return "", err 409 | } 410 | defer os.Remove(f.Name()) 411 | if _, err := f.Write(decodedPayload); err != nil { 412 | return "", err 413 | } 414 | if err := f.Close(); err != nil { 415 | return "", err 416 | } 417 | 418 | output, err := exec.Command("jlutil", f.Name()).CombinedOutput() 419 | if err != nil { 420 | return "", err 421 | } 422 | 423 | encodedOutput, err := json.Marshal(string(output)) 424 | if err != nil { 425 | return "", err 426 | } 427 | 428 | return string(encodedOutput[1 : len(encodedOutput)-1]), nil 429 | } 430 | 431 | func listToRegex(ls []string) []*regexp.Regexp { 432 | rex := make([]*regexp.Regexp, len(ls)) 433 | for i, item := range ls { 434 | replaced := strings.ReplaceAll(item, "*", ".*") 435 | r := regexp.MustCompile(replaced) 436 | rex[i] = r 437 | } 438 | return rex 439 | } 440 | 441 | func setupFlags() { 442 | rootCmd.Flags().StringP("id", "i", "", "connect to device with ID") 443 | rootCmd.Flags().StringP("remote", "r", "", "connect to device at IP address") 444 | rootCmd.Flags().StringP("name", "n", "", "process name") 445 | rootCmd.Flags().StringP("file", "f", "", "spawn the file") 446 | rootCmd.Flags().StringP("output", "o", "", "save output to this file") 447 | 448 | rootCmd.Flags().StringP("config", "c", "", "path to gxpc.conf file; default user home directory") 449 | 450 | rootCmd.Flags().StringSliceP("whitelist", "w", []string{}, "whitelist connection by name") 451 | rootCmd.Flags().StringSliceP("blacklist", "b", []string{}, "blacklist connection by name") 452 | rootCmd.Flags().StringSliceP("whitelistp", "", []string{}, "whitelist connection by PID") 453 | rootCmd.Flags().StringSliceP("blacklistp", "", []string{}, "blacklist connection by PID") 454 | 455 | rootCmd.Flags().BoolP("list", "l", false, "list available devices") 456 | //rootCmd.Flags().BoolP("decode", "d", true, "try to decode(bplist00 or bplist15), otherwise print base64 of bytes") 457 | //rootCmd.Flags().BoolP("hex", "x", false, "print hexdump of raw data") 458 | 459 | rootCmd.Flags().IntP("pid", "p", -1, "PID of wanted process") 460 | } 461 | 462 | func main() { 463 | setupFlags() 464 | logger = NewLogger() 465 | defer logger.Close() 466 | 467 | if err := rootCmd.Execute(); err != nil { 468 | logger.Errorf("Error ocurred: %v", err) 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /object.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | type ctr struct { 13 | c int 14 | l *sync.Mutex 15 | } 16 | 17 | var c = &ctr{l: &sync.Mutex{}} 18 | 19 | func PrintData(value any, decode, printHex bool, 20 | whitelist, blacklist, whitelistp, blacklistp []*regexp.Regexp, 21 | logger *Logger) { 22 | msg := 0 23 | 24 | val := reflect.ValueOf(value) 25 | 26 | data := make(map[string]any) 27 | 28 | if val.Kind() == reflect.Map { 29 | for _, elem := range val.MapKeys() { 30 | v := val.MapIndex(elem) 31 | data[elem.Interface().(string)] = v.Interface() 32 | } 33 | } 34 | name := data["connName"].(string) 35 | 36 | var pid float64 37 | if _, ok := data["pid"].(float64); ok { 38 | pid = data["pid"].(float64) 39 | } 40 | 41 | if len(whitelist) > 0 || len(blacklist) > 0 { 42 | if len(whitelist) > 0 && !connInList(name, whitelist) { 43 | return 44 | } else { 45 | if connInList(name, blacklist) { 46 | return 47 | } 48 | } 49 | } else { 50 | if pid > 0 { 51 | if len(whitelistp) > 0 && !pidInList(pid, whitelistp) { 52 | return 53 | } else { 54 | if pidInList(pid, blacklistp) { 55 | return 56 | } 57 | } 58 | } 59 | } 60 | 61 | c.l.Lock() 62 | msg = c.c 63 | c.c++ 64 | c.l.Unlock() 65 | 66 | var message string 67 | fnName := fmt.Sprintf("%d) Name: %s\n", msg, data["name"]) 68 | connName := fmt.Sprintf("Connection Name: %s\n", data["connName"]) 69 | if _, ok := data["dictionary"]; ok { 70 | printData(reflect.ValueOf(data["dictionary"]), "", "", &message) 71 | } 72 | total := len(fnName) + len(connName) + len(message) + 100 73 | 74 | builder := strings.Builder{} 75 | builder.Grow(total) 76 | 77 | builder.WriteString(fnName) 78 | builder.WriteString(connName) 79 | builder.WriteString("Data:\n") 80 | builder.WriteString(message) 81 | builder.WriteString(fmt.Sprintf("\n%s\n", strings.Repeat("=", 80))) 82 | 83 | logger.Scriptf("%d) Name: %s", msg, data["name"]) 84 | logger.Scriptf("Connection Name: %s", data["connName"]) 85 | pid, ok := data["pid"].(float64) 86 | if ok { 87 | logger.Scriptf("PID: %d", int(pid)) 88 | } 89 | logger.Scriptf("Data:") 90 | logger.Scriptf("%s", message) 91 | fmt.Println(strings.Repeat("=", 80)) 92 | 93 | logger.writeToFileScript(builder.String()) 94 | 95 | } 96 | 97 | func printData(v reflect.Value, key, indent string, message *string) { 98 | if v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer { 99 | v = v.Elem() 100 | } 101 | 102 | switch v.Kind() { 103 | case reflect.Map: 104 | if key != "" { 105 | *message += fmt.Sprintf("%s%s => \n", indent, key) 106 | } else { 107 | *message += fmt.Sprintf("") 108 | } 109 | for _, k := range v.MapKeys() { 110 | printData(v.MapIndex(k), k.Interface().(string), indent+"\t", message) 111 | } 112 | case reflect.Array, reflect.Slice: 113 | *message += fmt.Sprintf("%s%s => [\n", indent, key) 114 | *message += indent + "[\n" 115 | for i := 0; i < v.Len(); i++ { 116 | keyNum := strconv.Itoa(i) 117 | printData(v.Index(i), keyNum, indent+"\t", message) 118 | } 119 | *message += indent + "]\n" 120 | default: 121 | if key != "" { 122 | *message += fmt.Sprintf("%s%s => %v\n", indent, key, v.Interface()) 123 | } else { 124 | *message += fmt.Sprintf("%s => %v\n", indent, v.Interface()) 125 | } 126 | } 127 | } 128 | 129 | func connInList(connName string, list []*regexp.Regexp) bool { 130 | for _, b := range list { 131 | if match := b.MatchString(connName); match { 132 | return true 133 | } 134 | } 135 | return false 136 | } 137 | 138 | func pidInList(pid float64, list []*regexp.Regexp) bool { 139 | ps := fmt.Sprintf("%f", pid) 140 | for _, b := range list { 141 | if match := b.MatchString(ps); match { 142 | return true 143 | } 144 | } 145 | return false 146 | } 147 | -------------------------------------------------------------------------------- /offsets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "os" 7 | ) 8 | 9 | type BuildData struct { 10 | PlistCreate string `json:"PlistCreate"` 11 | CallHandler string `json:"CallHandler"` 12 | } 13 | 14 | type Offset struct { 15 | OS string `json:"os"` 16 | Builds []map[string]BuildData `json:"builds"` 17 | } 18 | 19 | type OffsetsData struct { 20 | Offsets []Offset `json:"offsets"` 21 | } 22 | 23 | type NewOffset struct { 24 | Machine string `json:"machine"` 25 | Version string `json:"version"` 26 | CallEvent string `json:"callEvent"` 27 | PlistCreate string `json:"plistCreate"` 28 | } 29 | 30 | func updateConfig(configPath string, off *NewOffset) error { 31 | // there is no config file yet created, create one and append data to it 32 | if _, err := os.Stat(configPath); os.IsNotExist(err) { 33 | f, err := os.Create(configPath) 34 | if err != nil { 35 | return err 36 | } 37 | defer f.Close() 38 | configData := OffsetsData{ 39 | Offsets: []Offset{ 40 | { 41 | OS: off.Machine, 42 | Builds: []map[string]BuildData{ 43 | { 44 | off.Version: { 45 | PlistCreate: off.PlistCreate, 46 | CallHandler: off.CallEvent, 47 | }, 48 | }, 49 | }, 50 | }, 51 | }, 52 | } 53 | enc := json.NewEncoder(f) 54 | enc.SetIndent("", " ") 55 | return enc.Encode(configData) 56 | } else { 57 | var configData OffsetsData 58 | f, err := os.OpenFile(configPath, os.O_RDWR, 644) 59 | if err != nil { 60 | return err 61 | } 62 | defer f.Close() 63 | if err := json.NewDecoder(f).Decode(&configData); err != nil { 64 | return err 65 | } 66 | 67 | // TODO: we need to implement a check for different builds for the same platform 68 | configData.Offsets = append(configData.Offsets, Offset{ 69 | OS: off.Machine, 70 | Builds: []map[string]BuildData{ 71 | { 72 | off.Version: { 73 | PlistCreate: off.PlistCreate, 74 | CallHandler: off.CallEvent, 75 | }, 76 | }, 77 | }, 78 | }) 79 | 80 | f.Truncate(0) 81 | f.Seek(0, io.SeekStart) 82 | enc := json.NewEncoder(f) 83 | enc.SetIndent("", " ") 84 | return enc.Encode(configData) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gxpc", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "gxpc", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "frida-objc-bridge": "^8.0.4" 13 | } 14 | }, 15 | "node_modules/frida-objc-bridge": { 16 | "version": "8.0.4", 17 | "resolved": "https://registry.npmjs.org/frida-objc-bridge/-/frida-objc-bridge-8.0.4.tgz", 18 | "integrity": "sha512-AUzwq/+TFtblQLTI97UPX22pBcvyG2oUde9380DXbcJ0zXts4n9JDVONHUr03aBowa0Le+bdBoVdCs7jyyjvlg==", 19 | "license": "LGPL-2.0 WITH WxWindows-exception-3.1" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gxpc", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "description": "", 11 | "dependencies": { 12 | "frida-objc-bridge": "^8.0.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReverseApple/gxpc/67b3e575b5d8a1fe9115cfd39e0affbf03d477b7/running.png -------------------------------------------------------------------------------- /running_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReverseApple/gxpc/67b3e575b5d8a1fe9115cfd39e0affbf03d477b7/running_one.png -------------------------------------------------------------------------------- /script.ts: -------------------------------------------------------------------------------- 1 | import ObjC from "frida-objc-bridge"; 2 | 3 | const LIBXPC_PATH = '/usr/lib/system/libxpc.dylib'; 4 | let libxpc_dylib = Process.getModuleByName(LIBXPC_PATH); 5 | 6 | // ObjC classes 7 | const { 8 | NSData, 9 | NSPropertyListSerialization, 10 | NSXPCDecoder, 11 | } = ObjC.classes; 12 | 13 | // Intercept these functions 14 | const xpc_connection_send_notification = libxpc_dylib.getExportByName("xpc_connection_send_notification"); 15 | const xpc_connection_send_message = libxpc_dylib.getExportByName("xpc_connection_send_message"); 16 | const xpc_connection_send_message_with_reply = libxpc_dylib.getExportByName("xpc_connection_send_message_with_reply"); 17 | const xpc_connection_send_message_with_reply_sync = libxpc_dylib.getExportByName("xpc_connection_send_message_with_reply_sync"); 18 | const xpc_connection_create_mach_service = libxpc_dylib.getExportByName("xpc_connection_create_mach_service"); 19 | const xpc_connection_set_event_handler = libxpc_dylib.getExportByName("xpc_connection_set_event_handler"); 20 | 21 | const sysctlbyname_addr = Module.getGlobalExportByName('sysctlbyname'); 22 | const sysctlbyname = new NativeFunction(sysctlbyname_addr, 'int', ['pointer', 'pointer', 'pointer', 'pointer', 'int']); 23 | 24 | let __CFBinaryPlistCreate15: NativePointer; 25 | let _xpc_connection_call_event_handler: NativePointer; 26 | let CFBinaryPlistCreate15: NativeFunction; 27 | let xpc_connection_call_event_handler: NativeFunction; 28 | 29 | 30 | // Use these functions to make sense out of xpc_object_t and xpc_connection_t 31 | const xpc_connection_get_name: NativeFunction = getFunc("xpc_connection_get_name", "pointer", ["pointer"]); 32 | const xpc_get_type: NativeFunction = getFunc("xpc_get_type", "pointer", ["pointer"]); 33 | const xpc_dictionary_get_value: NativeFunction = getFunc("xpc_dictionary_get_value", "pointer", ["pointer", "pointer"]); 34 | const xpc_string_get_string_ptr: NativeFunction = getFunc("xpc_string_get_string_ptr", "pointer", ["pointer"]); 35 | const xpc_copy_description: NativeFunction = getFunc("xpc_copy_description", "pointer", ["pointer"]); 36 | 37 | const xpc_uint64_get_value:NativeFunction = getFunc("xpc_uint64_get_value", "int", ["pointer"]); 38 | const xpc_int64_get_value:NativeFunction = getFunc("xpc_int64_get_value", "int", ["pointer"]); 39 | const xpc_double_get_value:NativeFunction = getFunc("xpc_double_get_value", "double", ["pointer"]); 40 | const xpc_bool_get_value:NativeFunction = getFunc("xpc_bool_get_value", "bool", ["pointer"]); 41 | const xpc_uuid_get_bytes:NativeFunction = getFunc("xpc_uuid_get_bytes", "pointer", ["pointer"]); 42 | 43 | const xpc_array_get_count:NativeFunction = getFunc("xpc_array_get_count", "int", ["pointer"]); 44 | const xpc_array_get_value:NativeFunction = getFunc("xpc_array_get_value", "pointer", ["pointer", "int"]); 45 | 46 | const xpc_data_get_length:NativeFunction = getFunc("xpc_data_get_length", "int", ["pointer"]); 47 | const xpc_data_get_bytes:NativeFunction = getFunc("xpc_data_get_bytes", "int", ["pointer", "pointer", "int", "int"]); 48 | 49 | const xpc_date_get_value:NativeFunction = getFunc("xpc_date_get_value", "int64", ["pointer"]); 50 | 51 | const xpc_connection_get_pid:NativeFunction = getFunc("xpc_connection_get_pid", "int", ["pointer"]); 52 | 53 | const xpc_type_activity = getPtr("_xpc_type_activity"); 54 | const xpc_type_array = getPtr("_xpc_type_array"); 55 | const xpc_type_base = getPtr("_xpc_type_base"); 56 | const xpc_type_bool = getPtr("_xpc_type_bool"); 57 | const xpc_type_bundle = getPtr("_xpc_type_bundle"); 58 | const xpc_type_connection = getPtr("_xpc_type_connection"); 59 | const xpc_type_data = getPtr("_xpc_type_data"); 60 | const xpc_type_date = getPtr("_xpc_type_date"); 61 | const xpc_type_dictionary = getPtr("_xpc_type_dictionary"); 62 | const xpc_type_double = getPtr("_xpc_type_double"); 63 | const xpc_type_endpoint = getPtr("_xpc_type_endpoint"); 64 | const xpc_type_error = getPtr("_xpc_type_error"); 65 | const xpc_type_fd = getPtr("_xpc_type_fd"); 66 | const xpc_type_file_transfer = getPtr("_xpc_type_file_transfer"); 67 | const xpc_type_int64 = getPtr("_xpc_type_int64"); 68 | const xpc_type_mach_recv = getPtr("_xpc_type_mach_recv"); 69 | const xpc_type_mach_send = getPtr("_xpc_type_mach_send"); 70 | const xpc_type_null = getPtr("_xpc_type_null"); 71 | const xpc_type_pipe = getPtr("_xpc_type_pipe"); 72 | const xpc_type_pointer = getPtr("_xpc_type_pointer"); 73 | const xpc_type_serializer = getPtr("_xpc_type_serializer"); 74 | const xpc_type_service = getPtr("_xpc_type_service"); 75 | const xpc_type_service_instance = getPtr("_xpc_type_service_instance"); 76 | const xpc_type_shmem = getPtr("_xpc_type_shmem"); 77 | const xpc_type_string = getPtr("_xpc_type_string"); 78 | const xpc_type_uint64 = getPtr("_xpc_type_uint64"); 79 | const xpc_type_uuid = getPtr("_xpc_type_uuid"); 80 | 81 | // helper function that will create new NativeFunction 82 | function getFunc(name: any, ret_type: any, args: any) { 83 | return new NativeFunction(Module.getGlobalExportByName( name), ret_type, args); 84 | } 85 | 86 | // helper function that will create new NativePointer 87 | function getPtr(name: string) { 88 | return new NativePointer(Module.getGlobalExportByName(name)); 89 | } 90 | 91 | // create C string from JavaScript string 92 | function cstr(str: string) { 93 | return Memory.allocUtf8String(str); 94 | } 95 | 96 | // get JavaScript string from C string 97 | function rcstr(cstr: NativePointer): any { 98 | return cstr.readCString(); 99 | } 100 | 101 | // get value type name from xpc_object_t 102 | function getValueTypeName(val: NativePointer) { 103 | let valueType = xpc_get_type(val); 104 | if (xpc_type_activity.equals(valueType)) 105 | return "activity"; 106 | if (xpc_type_array.equals(valueType)) 107 | return "array"; 108 | if (xpc_type_base.equals(valueType)) 109 | return "base"; 110 | if (xpc_type_bool.equals(valueType)) 111 | return "bool"; 112 | if (xpc_type_bundle.equals(valueType)) 113 | return "bundle"; 114 | if (xpc_type_connection.equals(valueType)) 115 | return "connection"; 116 | if (xpc_type_data.equals(valueType)) 117 | return "data"; 118 | if (xpc_type_date.equals(valueType)) 119 | return "date"; 120 | if (xpc_type_dictionary.equals(valueType)) 121 | return "dictionary"; 122 | if (xpc_type_double.equals(valueType)) 123 | return "double"; 124 | if (xpc_type_endpoint.equals(valueType)) 125 | return "endpoint"; 126 | if (xpc_type_error.equals(valueType)) 127 | return "error"; 128 | if (xpc_type_fd.equals(valueType)) 129 | return "fd"; 130 | if (xpc_type_file_transfer.equals(valueType)) 131 | return "file_transfer"; 132 | if (xpc_type_int64.equals(valueType)) 133 | return "int64"; 134 | if (xpc_type_mach_recv.equals(valueType)) 135 | return "mach_recv"; 136 | if (xpc_type_mach_send.equals(valueType)) 137 | return "mach_send"; 138 | if (xpc_type_null.equals(valueType)) 139 | return "null"; 140 | if (xpc_type_pipe.equals(valueType)) 141 | return "pipe"; 142 | if (xpc_type_pointer.equals(valueType)) 143 | return "pointer"; 144 | if (xpc_type_serializer.equals(valueType)) 145 | return "serializer"; 146 | if (xpc_type_service.equals(valueType)) 147 | return "service"; 148 | if (xpc_type_service_instance.equals(valueType)) 149 | return "service_instance"; 150 | if (xpc_type_shmem.equals(valueType)) 151 | return "shmem"; 152 | if (xpc_type_string.equals(valueType)) 153 | return "string"; 154 | if (xpc_type_uint64.equals(valueType)) 155 | return "uint64"; 156 | if (xpc_type_uuid.equals(valueType)) 157 | return "uuid"; 158 | return null; 159 | } 160 | 161 | // get C string from XPC string 162 | function getXPCString(val: NativePointer) { 163 | let content = xpc_string_get_string_ptr(val); 164 | return rcstr(content) 165 | } 166 | 167 | // get human-readable date from Unix timestamp 168 | function getXPCDate(val: NativePointer) { 169 | let nanoseconds = xpc_date_get_value(val); 170 | 171 | // Convert nanoseconds to milliseconds 172 | const timestampInMilliseconds = nanoseconds / 1000000; 173 | 174 | // Create a JavaScript Date object in UTC 175 | const date = new Date(timestampInMilliseconds); 176 | 177 | return { 178 | iso: date.toISOString(), 179 | nanoseconds: nanoseconds, 180 | }; 181 | } 182 | 183 | function getXPCData(conn: NativePointer, dict: any, buff: NativePointer, n: any) { 184 | const hdr = buff.readCString(8); 185 | if (hdr == "bplist15") { 186 | const plist = CFBinaryPlistCreate15(buff, n, NULL); 187 | return new ObjC.Object(plist).description().toString(); 188 | } else if (hdr == "bplist16") { 189 | let ObjCData = NSData.dataWithBytes_length_(buff, n); 190 | let base64Encoded = ObjCData.base64EncodedStringWithOptions_(0).toString(); 191 | 192 | send(JSON.stringify({ 193 | "type": "jlutil", 194 | "payload": base64Encoded, 195 | })); 196 | 197 | let resp; 198 | recv("jlutil", (message, _) => { 199 | resp = message.payload; 200 | }) 201 | .wait(); 202 | if (resp) { 203 | return resp; 204 | } 205 | 206 | if (conn != null) { 207 | return parseBPList(conn, dict); 208 | } 209 | 210 | return base64Encoded; 211 | } else if (hdr == "bplist17") { 212 | if (conn != null) { 213 | return parseBPList(conn, dict); 214 | } 215 | } else if (hdr == "bplist00") { 216 | const format = Memory.alloc(8); 217 | format.writeU64(0xaaaaaaaa); 218 | let ObjCData = NSData.dataWithBytes_length_(buff, n); 219 | const plist = NSPropertyListSerialization.propertyListWithData_options_format_error_(ObjCData, 0, format, NULL); 220 | return new ObjC.Object(plist).description().toString(); 221 | } 222 | 223 | let ObjCData = NSData.dataWithBytes_length_(buff, n); 224 | let base64Encoded = ObjCData.base64EncodedStringWithOptions_(0).toString(); 225 | return base64Encoded; 226 | } 227 | 228 | function getKeys(description: any) { 229 | const rex = /(.*?)"\s=>\s/g; 230 | let matches = (description.match(rex) || []).map((e: string) => e.replace(rex, '$1')); 231 | let realMatches = []; 232 | let first = true; 233 | let depth = 0; 234 | for (let i in matches) { 235 | if (first) { 236 | depth = (matches[i].match(/\t/g) || []).length; 237 | first = false; 238 | } 239 | let elemDepth = (matches[i].match(/\t/g) || []).length; 240 | if (elemDepth == depth) { 241 | realMatches.push(matches[i].slice(2)); 242 | } 243 | } 244 | return realMatches; 245 | } 246 | 247 | // https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/Foundation.framework/NSXPCDecoder.h 248 | function parseBPList(conn: NativePointer, dict: NativePointer) { 249 | let decoder = NSXPCDecoder.alloc().init(); 250 | try { 251 | decoder["- _setConnection:"](conn); 252 | } catch (err) { 253 | decoder["- set_connection:"](conn); 254 | } 255 | decoder["- _startReadingFromXPCObject:"](dict); 256 | let debugDescription = decoder.debugDescription(); 257 | decoder.dealloc(); 258 | return debugDescription.toString(); 259 | } 260 | 261 | function extract(conn: NativePointer, xpc_object: NativePointer, dict: any): any { 262 | let ret: any = {}; 263 | let xpc_object_type = getValueTypeName(xpc_object); 264 | switch (xpc_object_type) { 265 | case "dictionary": 266 | ret = {}; 267 | dict = xpc_object; 268 | let keys: string[] = getKeys(rcstr(xpc_copy_description(xpc_object))); 269 | for (let i in keys) { 270 | let val = xpc_dictionary_get_value(dict, cstr(keys[i])); 271 | ret[keys[i]] = extract(conn, val, dict); 272 | } 273 | return ret; 274 | case "bool": 275 | return xpc_bool_get_value(xpc_object); 276 | case "uuid": 277 | return xpc_uuid_get_bytes(xpc_object); 278 | case "double": 279 | return xpc_double_get_value(xpc_object); 280 | case "string": 281 | return getXPCString(xpc_object); 282 | case "data": 283 | let dataLen = xpc_data_get_length(xpc_object); 284 | if (dataLen > 0) { 285 | let buff = Memory.alloc(Process.pointerSize * dataLen); 286 | let n = xpc_data_get_bytes(xpc_object, buff, 0, dataLen); 287 | return getXPCData(conn, dict, buff, n); 288 | } else { 289 | let empty = new Uint8Array(); 290 | return empty; 291 | } 292 | case "uint64": 293 | return xpc_uint64_get_value(xpc_object); 294 | case "int64": 295 | return xpc_int64_get_value(xpc_object); 296 | case "date": 297 | return getXPCDate(xpc_object); 298 | case "array": 299 | ret = []; 300 | let count = xpc_array_get_count(xpc_object); 301 | for (let j = 0; j < count; j++) { 302 | let elem = xpc_array_get_value(xpc_object, j); 303 | let el = extract(conn, elem, null); 304 | ret.push(el); 305 | } 306 | return ret; 307 | case "null": 308 | return "null-object"; 309 | default: 310 | return {}; 311 | } 312 | } 313 | 314 | let ps = new NativeCallback((fnName, conn, dict) => { 315 | let ret: any = {}; 316 | let fname: string = rcstr(fnName); 317 | ret["name"] = fname; 318 | ret["connName"] = "UNKNOWN"; 319 | ret["pid"] = xpc_connection_get_pid(conn); 320 | if (conn != null) { 321 | let connName = xpc_connection_get_name(conn); 322 | if (! connName.isNull()) { 323 | ret["connName"] = rcstr(connName); 324 | } 325 | } 326 | if (fname == "xpc_connection_set_event_handler") { 327 | let data = {"blockImplementation": dict.toString()}; 328 | ret["dictionary"] = data; 329 | } else { 330 | ret["dictionary"] = extract(conn, dict, dict); 331 | } 332 | send(JSON.stringify({"type": "print", "payload": ret})); 333 | }, "void", ["pointer", "pointer", "pointer"]); 334 | 335 | let cm_notification = new CModule(` 336 | #include 337 | extern void ps(void*,void*,void*); 338 | 339 | void onEnter(GumInvocationContext * ic) 340 | { 341 | void * conn = gum_invocation_context_get_nth_argument(ic,0); 342 | void * obj = gum_invocation_context_get_nth_argument(ic,1); 343 | ps("xpc_connection_send_notification", conn, obj); 344 | } 345 | `, {ps}); 346 | 347 | let cm_send_message = new CModule(` 348 | #include 349 | extern void ps(void*,void*,void*); 350 | 351 | void onEnter(GumInvocationContext * ic) 352 | { 353 | void * conn = gum_invocation_context_get_nth_argument(ic,0); 354 | void * obj = gum_invocation_context_get_nth_argument(ic,1); 355 | ps("xpc_connection_send_message", conn, obj); 356 | } 357 | `, {ps}); 358 | 359 | let cm_send_message_with_reply = new CModule(` 360 | #include 361 | extern void ps(void*,void*,void*); 362 | 363 | void onEnter(GumInvocationContext * ic) 364 | { 365 | void * conn = gum_invocation_context_get_nth_argument(ic,0); 366 | void * obj = gum_invocation_context_get_nth_argument(ic,1); 367 | ps("xpc_connection_send_message_with_reply", conn, obj); 368 | } 369 | `, {ps}); 370 | 371 | let cm_send_message_with_reply_sync = new CModule(` 372 | #include 373 | extern void ps(void*,void*,void*); 374 | 375 | void onEnter(GumInvocationContext * ic) 376 | { 377 | void * conn = gum_invocation_context_get_nth_argument(ic,0); 378 | void * obj = gum_invocation_context_get_nth_argument(ic,1); 379 | ps("xpc_connection_send_message_with_reply_sync", conn, obj); 380 | } 381 | `, {ps}); 382 | 383 | let cm_call_event_handler = new CModule(` 384 | #include 385 | extern void ps(void*,void*,void*); 386 | 387 | void onEnter(GumInvocationContext * ic) 388 | { 389 | void * conn = gum_invocation_context_get_nth_argument(ic,0); 390 | void * obj = gum_invocation_context_get_nth_argument(ic,1); 391 | ps("xpc_connection_call_event_handler", conn, obj); 392 | } 393 | `, {ps}); 394 | 395 | let psize = Memory.alloc(Process.pointerSize); 396 | psize.writeInt(Process.pointerSize * 2); 397 | 398 | let cm_set_event_handler = new CModule(` 399 | #include 400 | extern int pointerSize; 401 | extern void ps(void*,void*,void*); 402 | 403 | void onEnter(GumInvocationContext * ic) 404 | { 405 | void * conn = gum_invocation_context_get_nth_argument(ic,0); 406 | void * obj = gum_invocation_context_get_nth_argument(ic,1); 407 | void * impl = obj + (pointerSize*2); 408 | ps("xpc_connection_set_event_handler", conn, impl); 409 | } 410 | `, {pointerSize: psize, ps}); 411 | 412 | 413 | // @ts-ignore 414 | Interceptor.attach(xpc_connection_send_notification, cm_notification); 415 | // @ts-ignore 416 | Interceptor.attach(xpc_connection_send_message, cm_send_message); 417 | // @ts-ignore 418 | Interceptor.attach(xpc_connection_send_message_with_reply, cm_send_message_with_reply); 419 | // @ts-ignore 420 | Interceptor.attach(xpc_connection_send_message_with_reply_sync, cm_send_message_with_reply_sync); 421 | 422 | Interceptor.attach(xpc_connection_create_mach_service, { 423 | onEnter(args) { 424 | let ret: any = {}; 425 | ret["connName"] = rcstr(args[0]); 426 | ret["name"] = "xpc_connection_create_mach_service"; 427 | ret["dictionary"] = { 428 | "Service name": rcstr(args[0]) 429 | }; 430 | send(JSON.stringify({"type": "print", "payload": ret})); 431 | }, 432 | }); 433 | 434 | function sysctl(name: string) { 435 | const size = Memory.alloc(0x4); 436 | sysctlbyname(Memory.allocUtf8String(name), ptr(0), size, ptr(0), 0); 437 | const value = Memory.alloc(size.readU32()); 438 | sysctlbyname(Memory.allocUtf8String(name), value, size, ptr(0), 0); 439 | return value.readCString(); 440 | } 441 | 442 | let timerID = setInterval(function() { 443 | if (__CFBinaryPlistCreate15 != null && _xpc_connection_call_event_handler != null) { 444 | CFBinaryPlistCreate15 = new NativeFunction(__CFBinaryPlistCreate15, "pointer", ["pointer", "int", "pointer"]); 445 | xpc_connection_call_event_handler = new NativeFunction(_xpc_connection_call_event_handler, "void", ["pointer", "pointer"]); 446 | setImmediate(function() { 447 | // @ts-ignore 448 | Interceptor.attach(xpc_connection_call_event_handler, cm_call_event_handler); 449 | // @ts-ignore 450 | Interceptor.attach(xpc_connection_set_event_handler, cm_set_event_handler); 451 | }); 452 | } 453 | }, 1000); 454 | 455 | 456 | rpc.exports = { 457 | setup(offsets) { 458 | const machine = sysctl("hw.machine"); 459 | const osversion = sysctl("kern.osversion"); 460 | 461 | 462 | let found = false; 463 | 464 | if (offsets != null) { 465 | for (let i = 0; i < offsets.offsets.length; i++) { 466 | let os = offsets.offsets[i].os; 467 | if (os == machine) { 468 | for (let j = 0; j < offsets.offsets[i].builds.length; j++) { 469 | let buildData = offsets.offsets[i].builds[j]; 470 | let build = Object.keys(buildData)[0]; 471 | if (build == osversion) { 472 | let plistCreate = buildData[build]["PlistCreate"]; 473 | let callHandler = buildData[build]["CallHandler"]; 474 | __CFBinaryPlistCreate15 = Process.getModuleByName('CoreFoundation').base.add(Number(plistCreate)); 475 | _xpc_connection_call_event_handler = Process.getModuleByName('libxpc.dylib').base.add(Number(callHandler)); 476 | found = true; 477 | break; 478 | } 479 | } 480 | } 481 | } 482 | } 483 | 484 | if (!found) { 485 | __CFBinaryPlistCreate15 = DebugSymbol.fromName('__CFBinaryPlistCreate15').address; 486 | _xpc_connection_call_event_handler = DebugSymbol.fromName("_xpc_connection_call_event_handler").address; 487 | 488 | send(JSON.stringify({ 489 | "type": "newOffset", 490 | "machine": machine, 491 | "version": osversion, 492 | "plistCreate": __CFBinaryPlistCreate15.sub(Process.getModuleByName('CoreFoundation').base), 493 | "callEvent": _xpc_connection_call_event_handler.sub(Process.getModuleByName('libxpc.dylib').base) 494 | })); 495 | } 496 | 497 | return null; 498 | }, 499 | } 500 | --------------------------------------------------------------------------------