Skip to content

Commit

Permalink
Feature/process list (#25)
Browse files Browse the repository at this point in the history
* add boilerplate code for deviceinfo service

* add commandline command 'ps' to main.go

* added support for JSON decoding and printing

* expose time.Time from NSDate struct, fix crash on missing startDate value for mach_kernel process

* fix defragmenting bug in decoder.go, forgot to add the actual 32 bytes of message to the fragmentBytes field

* Add Unit test with an actual fragmented message

* add comments and a Close function
  • Loading branch information
danielpaulus authored Apr 5, 2021
1 parent f6f3d1a commit 605cb19
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 7 deletions.
28 changes: 28 additions & 0 deletions ios/dtx_codec/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Channel struct {
connection *Connection
messageDispatcher Dispatcher
responseWaiters map[int]chan Message
defragmenters map[int]*FragmentDecoder
registeredMethods map[string]chan Message
mutex sync.Mutex
}
Expand Down Expand Up @@ -128,6 +129,33 @@ func (d *Channel) Dispatch(msg Message) {
if msg.ConversationIndex > 0 {
d.mutex.Lock()
defer d.mutex.Unlock()
if msg.IsFirstFragment() {
d.defragmenters[msg.Identifier] = NewFragmentDecoder(msg)
SendAckIfNeeded(d.connection, msg)
return
}
if msg.IsFragment() {
if defragmenter, ok := d.defragmenters[msg.Identifier]; ok {
defragmenter.AddFragment(msg)
if msg.IsLastFragment() {
messagesBytes := defragmenter.Extract()
msg, leftover, err := DecodeNonBlocking(messagesBytes)
if len(leftover) != 0 {
log.Error("Decoding fragmented message failed")
}
if err != nil {
log.Error("decoding framente")
}
d.responseWaiters[msg.Identifier] <- msg
delete(d.responseWaiters, msg.Identifier)
}
return
}
log.Warn("received message fragment without first message, dropping it")
delete(d.responseWaiters, msg.Identifier)
return
}

d.responseWaiters[msg.Identifier] <- msg
delete(d.responseWaiters, msg.Identifier)
return
Expand Down
6 changes: 3 additions & 3 deletions ios/dtx_codec/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func NewConnection(device ios.DeviceEntry, serviceName string) (*Connection, err
//The global channel is automatically present and used for requesting other channels and some other methods like notifyPublishedCapabilities
globalChannel := Channel{channelCode: 0,
messageIdentifier: 5, channelName: "global_channel", connection: dtxConnection,
messageDispatcher: NewGlobalDispatcher(requestChannelMessages, dtxConnection), responseWaiters: map[int]chan Message{}, registeredMethods: map[string]chan Message{}}
messageDispatcher: NewGlobalDispatcher(requestChannelMessages, dtxConnection), responseWaiters: map[int]chan Message{}, registeredMethods: map[string]chan Message{}, defragmenters: map[int]*FragmentDecoder{}}
dtxConnection.globalChannel = &globalChannel
go reader(dtxConnection)

Expand Down Expand Up @@ -148,7 +148,7 @@ func (dtxConn *Connection) ForChannelRequest(messageDispatcher Dispatcher) *Chan
//code := msg.Auxiliary.GetArguments()[0].(uint32)
identifier, _ := nskeyedarchiver.Unarchive(msg.Auxiliary.GetArguments()[1].([]byte))
//TODO: Setting the channel code here manually to -1 for making testmanagerd work. For some reason it requests the TestDriver proxy channel with code 1 but sends messages on -1. Should probably be fixed somehow
channel := &Channel{channelCode: -1, channelName: identifier[0].(string), messageIdentifier: 1, connection: dtxConn, messageDispatcher: messageDispatcher, responseWaiters: map[int]chan Message{}}
channel := &Channel{channelCode: -1, channelName: identifier[0].(string), messageIdentifier: 1, connection: dtxConn, messageDispatcher: messageDispatcher, responseWaiters: map[int]chan Message{}, defragmenters: map[int]*FragmentDecoder{}}
dtxConn.activeChannels[-1] = channel
return channel
}
Expand All @@ -174,7 +174,7 @@ func (dtxConn *Connection) RequestChannelIdentifier(identifier string, messageDi
log.WithFields(log.Fields{"channel_id": identifier, "error": err}).Error("failed requesting channel")
}
log.WithFields(log.Fields{"channel_id": identifier}).Debug("Channel open")
channel := &Channel{channelCode: code, channelName: identifier, messageIdentifier: 1, connection: dtxConn, messageDispatcher: messageDispatcher, responseWaiters: map[int]chan Message{}}
channel := &Channel{channelCode: code, channelName: identifier, messageIdentifier: 1, connection: dtxConn, messageDispatcher: messageDispatcher, responseWaiters: map[int]chan Message{}, defragmenters: map[int]*FragmentDecoder{}}
dtxConn.activeChannels[code] = channel
return channel
}
15 changes: 13 additions & 2 deletions ios/dtx_codec/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,20 @@ func ReadMessage(reader io.Reader) (Message, error) {
result := readHeader(header)

if result.IsFragment() {

//the first part of a fragmented message is only a header indicating the total length of
//the defragmented message
if result.IsFirstFragment() {
//put in the header as bytes here
result.fragmentBytes = header
return result, nil
}
//32 offset is correct, the binary starts with a payload header
messageBytes := make([]byte, result.MessageLength)
io.ReadFull(reader, messageBytes)
_, err := io.ReadFull(reader, messageBytes)
if err != nil {
return Message{}, err
}
result.fragmentBytes = messageBytes
return result, nil
}
Expand Down Expand Up @@ -227,7 +238,7 @@ func (d Message) IsFirstFragment() bool {

//IsLastFragment returns true if this message is the last fragment
func (d Message) IsLastFragment() bool {
return d.Fragments-d.FragmentIndex == 1
return d.Fragments > 1 && d.Fragments-d.FragmentIndex == 1
}

//IsFragment returns true if the Message is a fragment
Expand Down
66 changes: 66 additions & 0 deletions ios/dtx_codec/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,72 @@ func TestType1Message(t *testing.T) {

}

func TestFragmentedMessage(t *testing.T) {
dat, err := ioutil.ReadFile("fixtures/fragmentedmessage.bin")
if err != nil {
log.Fatal(err)
}

//test the non blocking decoder first
msg, remainingBytes, err := dtx.DecodeNonBlocking(dat)
if assert.NoError(t, err) {
assert.Equal(t, 79707, len(remainingBytes))
assert.Equal(t, uint16(3), msg.Fragments)
assert.Equal(t, uint16(0), msg.FragmentIndex)
assert.Equal(t, false, msg.HasPayload())
}
defragmenter := dtx.NewFragmentDecoder(msg)
msg, remainingBytes, err = dtx.DecodeNonBlocking(remainingBytes)
if assert.NoError(t, err) {
assert.Equal(t, 14171, len(remainingBytes))
assert.Equal(t, uint16(3), msg.Fragments)
assert.Equal(t, uint16(1), msg.FragmentIndex)
assert.Equal(t, false, msg.HasPayload())
}
defragmenter.AddFragment(msg)
msg, remainingBytes, err = dtx.DecodeNonBlocking(remainingBytes)
if assert.NoError(t, err) {
assert.Equal(t, 0, len(remainingBytes))
assert.Equal(t, uint16(3), msg.Fragments)
assert.Equal(t, uint16(2), msg.FragmentIndex)
assert.Equal(t, false, msg.HasPayload())
}
defragmenter.AddFragment(msg)
assert.True(t, defragmenter.HasFinished())
nonblockingFullMessage := defragmenter.Extract()

//now test that the blocking decoder creates the same message and that it is decodeable
dtxReader := bytes.NewReader(dat)
msg, err = dtx.ReadMessage(dtxReader)
if assert.NoError(t, err) {
assert.Equal(t, uint16(3), msg.Fragments)
assert.Equal(t, uint16(0), msg.FragmentIndex)
assert.Equal(t, false, msg.HasPayload())
assert.Equal(t, true, msg.IsFirstFragment())
}
defragmenter = dtx.NewFragmentDecoder(msg)
msg, err = dtx.ReadMessage(dtxReader)
if assert.NoError(t, err) {
assert.Equal(t, uint16(3), msg.Fragments)
assert.Equal(t, uint16(1), msg.FragmentIndex)
assert.Equal(t, true, msg.IsFragment())
}
defragmenter.AddFragment(msg)
msg, err = dtx.ReadMessage(dtxReader)
if assert.NoError(t, err) {
assert.Equal(t, uint16(3), msg.Fragments)
assert.Equal(t, uint16(2), msg.FragmentIndex)
assert.Equal(t, true, msg.IsLastFragment())
}
defragmenter.AddFragment(msg)
assert.Equal(t, true, defragmenter.HasFinished())
defraggedMessage := defragmenter.Extract()
assert.Equal(t, defraggedMessage, nonblockingFullMessage)
dtxReader = bytes.NewReader(defraggedMessage)
_, err = dtx.ReadMessage(dtxReader)
assert.NoError(t, err)
}

func TestDecoder(t *testing.T) {
dat, err := ioutil.ReadFile("fixtures/notifyOfPublishedCapabilites")
if err != nil {
Expand Down
Binary file added ios/dtx_codec/fixtures/fragmentedmessage.bin
Binary file not shown.
87 changes: 87 additions & 0 deletions ios/instruments/processlist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package instruments

import (
"time"

"github.com/danielpaulus/go-ios/ios"
dtx "github.com/danielpaulus/go-ios/ios/dtx_codec"
"github.com/danielpaulus/go-ios/ios/nskeyedarchiver"
log "github.com/sirupsen/logrus"
)

const deviceInfoServiceName = "com.apple.instruments.server.services.deviceinfo"

//ProcessInfo contains all the properties for a process
//running on an iOS devices that we get back from instruments
type ProcessInfo struct {
IsApplication bool
Name string
Pid uint64
RealAppName string
StartDate time.Time
}

//ProcessList returns a []ProcessInfo, one for each process running on the iOS device
func (p DeviceInfoService) ProcessList() ([]ProcessInfo, error) {
resp, err := p.channel.MethodCall("runningProcesses")
result := mapToProcInfo(resp.Payload[0].([]interface{}))
return result, err
}

//NameForPid resolves a process name for a given pid
func (p DeviceInfoService) NameForPid(pid uint64) error {
_, err := p.channel.MethodCall("execnameForPid:", pid)
return err
}

func mapToProcInfo(procList []interface{}) []ProcessInfo {
result := make([]ProcessInfo, len(procList))
for i, procMapInt := range procList {
procMap := procMapInt.(map[string]interface{})
procInf := ProcessInfo{}
procInf.IsApplication = procMap["isApplication"].(bool)
procInf.Name = procMap["name"].(string)
procInf.Pid = procMap["pid"].(uint64)
procInf.RealAppName = procMap["realAppName"].(string)
if date, ok := procMap["startDate"]; ok {
procInf.StartDate = date.(nskeyedarchiver.NSDate).Timestamp
}
result[i] = procInf

}
return result
}

type deviceInfoDispatcher struct {
conn *dtx.Connection
}

func (p deviceInfoDispatcher) Dispatch(m dtx.Message) {
dtx.SendAckIfNeeded(p.conn, m)
log.Debug(m)
}

//DeviceInfoService gives us access to retrieving process lists and resolving names for PIDs
type DeviceInfoService struct {
channel *dtx.Channel
conn *dtx.Connection
}

//NewDeviceInfoService creates a new DeviceInfoService for a given device
func NewDeviceInfoService(device ios.DeviceEntry) (*DeviceInfoService, error) {
dtxConn, err := dtx.NewConnection(device, serviceName)
if err != nil {
log.Debugf("Failed connecting to %s, trying %s", serviceName, serviceNameiOS14)
dtxConn, err = dtx.NewConnection(device, serviceNameiOS14)
if err != nil {
return nil, err
}
}
processControlChannel := dtxConn.RequestChannelIdentifier(deviceInfoServiceName, processControlDispatcher{dtxConn})
return &DeviceInfoService{channel: processControlChannel, conn: dtxConn}, nil
}

//Close closes up the DTX connection
func (d *DeviceInfoService) Close() {
d.conn.Close()
}
4 changes: 2 additions & 2 deletions ios/nskeyedarchiver/objectivec_classes.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ func NewNSError(object map[string]interface{}, objects []interface{}) interface{
const nsReferenceDate = 978307200000

type NSDate struct {
timestamp time.Time
Timestamp time.Time
}

type DTTapHeartbeatMessage struct {
Expand Down Expand Up @@ -320,7 +320,7 @@ func NewNSDate(object map[string]interface{}, objects []interface{}) interface{}
return NSDate{time}
}
func (n NSDate) String() string {
return fmt.Sprintf("%s", n.timestamp)
return fmt.Sprintf("%s", n.Timestamp)
}

type NSNull struct {
Expand Down
18 changes: 18 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Usage:
ios lang [--setlocale=<locale>] [--setlang=<newlang>] [options]
ios diagnostics list [options]
ios pair [options]
ios ps [options]
ios forward [options] <hostPort> <targetPort>
ios dproxy
ios readpair [options]
Expand Down Expand Up @@ -86,6 +87,7 @@ The commands work as following:
ios lang [--setlocale=<locale>] [--setlang=<newlang>] [options] Sets or gets the Device language
ios diagnostics list [options] List diagnostic infos
ios pair [options] Pairs the device without a dialog for supervised devices
ios ps [options] Dumps a list of running processes on the device
ios forward [options] <hostPort> <targetPort> Similar to iproxy, forward a TCP connection to the device.
ios dproxy Starts the reverse engineering proxy server. It dumps every communication in plain text so it can be implemented easily. Use "sudo launchctl unload -w /Library/Apple/System/Library/LaunchDaemons/com.apple.usbmuxd.plist" to stop usbmuxd and load to start it again should the proxy mess up things.
ios readpair Dump detailed information about the pairrecord for a device.
Expand Down Expand Up @@ -160,6 +162,12 @@ The commands work as following:
return
}

b, _ = arguments.Bool("ps")
if b {
processList(device)
return
}

b, _ = arguments.Bool("lang")
if b {
locale, _ := arguments.String("--setlocale")
Expand Down Expand Up @@ -485,6 +493,16 @@ func saveScreenshot(device ios.DeviceEntry, outputPath string) {
}
}

func processList(device ios.DeviceEntry) {
service, err := instruments.NewDeviceInfoService(device)
defer service.Close()
if err != nil {
failWithError("failed opening deviceInfoService for getting process list", err)
}
processList, err := service.ProcessList()
println(convertToJSONString(processList))
}

func printDeviceList(details bool) {
deviceList := ios.ListDevices()

Expand Down

0 comments on commit 605cb19

Please sign in to comment.