Files

314 lines
8.2 KiB
Go

package ssh
import (
_ "embed"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"path/filepath"
"github.com/blacktop/ipsw/internal/utils"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
)
var (
//go:embed data/debugserver.plist
entitlementsData []byte
//go:embed data/com.apple.system.logging.plist
loggingPlistData []byte
//go:embed data/com.apple.CrashReporter.plist
symbolicationPlistData []byte
)
func hostKeyCallback(path string) ssh.HostKeyCallback {
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
kh, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) //nolint:gomnd
if err != nil {
return fmt.Errorf("failed to open known_hosts: %w", err)
}
defer func() { _ = kh.Close() }()
callback, err := knownhosts.New(kh.Name())
if err != nil {
return fmt.Errorf("failed to check known_hosts: %w", err)
}
if err := callback(hostname, remote, key); err != nil {
var kerr *knownhosts.KeyError
if errors.As(err, &kerr) {
if len(kerr.Want) > 0 {
return fmt.Errorf("possible man-in-the-middle attack: %w", err)
}
// if want is empty, it means the host was not in the known_hosts file, so lets add it there.
fmt.Fprintln(kh, knownhosts.Line([]string{hostname}, key))
return nil
}
return fmt.Errorf("failed to check known_hosts: %w", err)
}
return nil
}
}
// Config is the configuration for an SSH connection
type Config struct {
Host string
Port string
User string
Pass string
Key string
Insecure bool
}
// SSH is an ssh object
type SSH struct {
client *ssh.Client
conf *Config
}
// NewSSH creates a new SSH connection
func NewSSH(conf *Config) (*SSH, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get user home directory: %w", err)
}
knownhostsPath := filepath.Join(home, ".ssh", "known_hosts")
var signer ssh.Signer
if len(conf.Key) > 0 {
key, err := os.ReadFile(conf.Key)
if err != nil {
return nil, fmt.Errorf("failed to read private key: %w", err)
}
signer, err = ssh.ParsePrivateKey(key)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
}
var sshConfig *ssh.ClientConfig
if conf.Insecure {
sshConfig = &ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{
ssh.Password(conf.Pass),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
} else {
sshConfig = &ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{
ssh.Password(conf.Pass),
},
HostKeyCallback: hostKeyCallback(knownhostsPath),
}
}
if signer != nil {
sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(signer))
}
client, err := ssh.Dial("tcp", conf.Host+":"+conf.Port, sshConfig)
if err != nil {
log.Fatalf("failed to dial: %s", err)
}
return &SSH{
client: client,
conf: conf,
}, nil
}
// Close closes the SSH connection
func (s *SSH) Close() error {
return s.client.Close()
}
// FileExists checks if a file exists on the remote device
func (s *SSH) FileExists(path string) bool {
session, err := s.client.NewSession()
if err != nil {
log.Fatalf("failed to create session: %s", err)
}
defer session.Close()
return session.Run(fmt.Sprintf("test -f %s", path)) == nil
}
// CopyToDevice copies a file to the remote device
func (s *SSH) CopyToDevice(src, dst string) error {
session, err := s.client.NewSession()
if err != nil {
log.Fatalf("failed to create session: %s", err)
}
defer session.Close()
f, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer f.Close()
go func() {
w, _ := session.StdinPipe()
defer w.Close()
count, err := io.Copy(w, f)
if err != nil {
log.Fatalf("failed to copy %s to device: %v", src, err)
}
if count == 0 {
log.Fatalf("%d bytes copied to device", count)
}
}()
if err := session.Start(fmt.Sprintf("cat > %s", dst)); err != nil {
return fmt.Errorf("failed to copy %s to %s on device: %w", src, dst, err)
}
if err := session.Wait(); err != nil {
return fmt.Errorf("failed to copy %s to device: %w", src, err)
}
return nil
}
// CopyFromDevice copies a file from the remote device
func (s *SSH) CopyFromDevice(src, dst string) error {
session, err := s.client.NewSession()
if err != nil {
log.Fatalf("failed to create session: %s", err)
}
defer session.Close()
f, err := os.Create(dst)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer f.Close()
go func() {
w, _ := session.StdoutPipe()
count, err := io.Copy(f, w)
if err != nil {
log.Fatalf("failed to copy %s from device: %v", src, err)
}
if count == 0 {
log.Fatalf("%d bytes copied to device", count)
}
}()
if err := session.Start(fmt.Sprintf("cat %s", src)); err != nil {
return fmt.Errorf("failed to copy %s from device to %s: %w", src, dst, err)
}
if err := session.Wait(); err != nil {
return fmt.Errorf("failed to copy %s from device: %w", src, err)
}
return nil
}
// RunCommand runs a command on the remote device
func (s *SSH) RunCommand(cmd string) error {
session, err := s.client.NewSession()
if err != nil {
log.Fatalf("failed to create session: %s", err)
}
defer session.Close()
err = session.Run(cmd)
if err != nil {
return fmt.Errorf("failed to run command: %w", err)
}
return nil
}
// RunCommandWithOutput runs a command on the remote device and returns the output
func (s *SSH) RunCommandWithOutput(cmd string) (string, error) {
session, err := s.client.NewSession()
if err != nil {
log.Fatalf("failed to create session: %s", err)
}
defer session.Close()
output, err := session.Output(cmd)
if err != nil {
return "", fmt.Errorf("failed to run command: %w", err)
}
return string(output), nil
}
// ResignDebugserver resigns debugserver with more powerful entitlements
func ResignDebugserver(dspath string) error {
entitlements, err := os.CreateTemp("", "entitlements.plist")
if err != nil {
return fmt.Errorf("failed to create tmp entitlements file: %w", err)
}
defer os.Remove(entitlements.Name()) // clean up
if _, err := entitlements.Write(entitlementsData); err != nil {
return fmt.Errorf("failed to write entitlements.plist data to tmp file: %w", err)
}
if err := entitlements.Close(); err != nil {
return fmt.Errorf("failed to close tmp entitlements.plist file: %w", err)
}
if err := utils.CodeSignWithEntitlements(dspath, entitlements.Name(), "-"); err != nil {
return fmt.Errorf("failed to codesign debugserver with entitlements: %w", err)
}
return nil
}
// EnablePrivateLogData enables private data in logs
// CREDIT - https://github.com/EthanArbuckle/unredact-private-os_logs
func (s *SSH) EnablePrivateLogData() error {
logging, err := os.CreateTemp("", "com.apple.system.logging.plist")
if err != nil {
return fmt.Errorf("failed to create tmp entitlements file: %w", err)
}
defer os.Remove(logging.Name()) // clean up
if _, err := logging.Write(loggingPlistData); err != nil {
return fmt.Errorf("failed to write com.apple.system.logging.plist data to tmp file: %w", err)
}
if err := logging.Close(); err != nil {
return fmt.Errorf("failed to close tmp com.apple.system.logging.plist file: %w", err)
}
return s.CopyToDevice(logging.Name(), "/Library/Preferences/Logging/com.apple.system.logging.plist")
}
// EnableSymbolication enables symbolication of mobile crash logs
// CREDIT - https://github.com/dlevi309/ios-scripts
func (s *SSH) EnableSymbolication() error {
crashReporter, err := os.CreateTemp("", "com.apple.CrashReporter.plist")
if err != nil {
return fmt.Errorf("failed to create tmp 'com.apple.CrashReporter.plist' file: %w", err)
}
defer os.Remove(crashReporter.Name()) // clean up
if _, err := crashReporter.Write(symbolicationPlistData); err != nil {
return fmt.Errorf("failed to write 'com.apple.CrashReporter.plist' data to tmp file: %w", err)
}
if err := crashReporter.Close(); err != nil {
return fmt.Errorf("failed to close tmp 'com.apple.CrashReporter.plist' file: %w", err)
}
return s.CopyToDevice(crashReporter.Name(), "/var/root/Library/Preferences/com.apple.CrashReporter.plist")
}
func (s *SSH) GetShshBlobs() ([]byte, error) {
session, err := s.client.NewSession()
if err != nil {
return nil, fmt.Errorf("failed to create ssh session: %w", err)
}
defer session.Close()
return session.Output("cat /dev/rdisk1 | dd bs=256 count=$((0x4000))")
}