脱獄をしてiPhoneを遠隔操作するプログラムを書く

最近は脱獄をする人がかなり減ってきたように感じられます。かつては脱獄をしなければ手に入らなかった便利な機能たちは、iOSのアップデートとともに徐々に追加され、脱獄をしなくても便利なOSへと進化を遂げました。
なので現在の僕にとって脱獄は、iPhoneを便利にするためのものではなく、単純に遊び道具としての魅力をもっています。そこで、今回は完全に実用性のない、iPhoneを遠隔操作するプログラムを開発してみたいと思います。

こんな感じで、操作側のiPhoneでドラッグやタップをすると遠隔操作されるプログラムを作成していきます！

https://twitter.com/ryu_hu03/status/1652686403489394689?s=61&t=7hSKlFGiu2BkYe7jHp8sfw


# 概要
遠隔操作と聞くと難しそうですが、実際のところ仕組みは単純です。
![スクリーンショット 2023-04-24 2.24.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627061/475a3267-cdb7-3ce2-1398-6937d1b66818.png)
図がわかりずらいのは置いといてこんな感じで、操作側iPhoneでドラッグを検知して座標をWebSocketサーバーに送信して、WebSocketサーバーはそれを受信したら操作される側のiPhoneに伝えます。
操作される側のiPhoneはそれをもとにタッチ処理を行う、という流れになっています。
なお、脱獄が必要なのは操作される側のiPhoneのみとなっています。

# 実装
まずはWebSocketサーバー側の実装からしていきましょう.　
## WebSocketサーバー
サーバーはnode.jsを使って書きました。
```JavaScript
var server = require('ws').Server
var wss = new server({
	host: '0.0.0.0',
	port: 8020
});

wss.on('connection', function(ws) {
	console.log("connected")

	ws.on('message', function(message) {
		console.log('received: %s', message);

		wss.clients.forEach(client => {
			client.send(message);
		});
	});
});

```
サーバー側はこんな感じのシンプルな実装になっています。
messageを受信したら、全てのclientにmessageを送信する処理となっています。

## 操作する側iPhone
コード簡略化のため、WebSocketClientライブラリにStarscreamを使用しました。
https://github.com/daltoniam/Starscream
```Swift
import SwiftUI
import Starscream

struct ContentView: View {
    @GestureState private var isDragging = false
    private let socket: WebSocket

    init() {
        let request = URLRequest(url: URL(string: "ws://192.168.0.33:8020")!)
        socket = WebSocket(request: request)
        socket.connect()
    }

    var body: some View {
        Color.white
            .gesture(
                DragGesture()
                    .updating($isDragging) { value, isDragging, _ in
                        defer { isDragging = true }
                        send(value.location, isDragging ? .touchMove : .touchDown)
                    }
                    .onEnded { value in
                        send(value.location, .touchUp)
                    }
            )
            .onTapGesture { location in
                send(location, .touch)
            }
    }

    func send(_ location: CGPoint, _ type: TouchType) {
        guard let data = "\(location.x),\(location.y),\(type.rawValue)".data(using: .utf8) else { return }
        socket.write(data: data, completion: nil)
    }
}

enum TouchType: Int {
    case touchDown, touchMove, touchUp, touch
}
```
こんな感じで、ドラッグをスタートした際は、
`x座標,y座標,0`
ドラッグ中は、
`x座標,y座標,1`
ドラッグ終了時は、
`x座標,y座標,2`
タッチした時は、
`x座標,y座標,3`
をサーバー側に送る処理が書かれています。

## 遠隔操作される側のiPhone
まずはタッチ処理をするプログラムをみなさんが大嫌いなObjective-Cで記述していきます。
ヘッダーファイルはこちらです。
```Objective-C:TouchSimulator.h
#ifndef TOUCH_SIMULATOR
#define TOUCH_SIMULATOR

#include "headers/IOHIDEvent.h"
#include "headers/IOHIDEventData.h"
#include "headers/IOHIDEventTypes.h"
#include "headers/IOHIDEventSystemClient.h"
#include "headers/IOHIDEventSystem.h"
#include <mach/mach_time.h>

#import <UIKit/UIApplication.h>
#import <UIKit/UIWindow.h>

@interface UIApplication()
-(void)_enqueueHIDEvent:(IOHIDEventRef)arg1;
@end

@interface UIWindow()
-(unsigned)_contextId;
@end

typedef enum {
    TOUCH_UP,
    TOUCH_DOWN,
    TOUCH_MOVE
} TouchType;

void simulateTouch(TouchType type, float x, float y);

#endif
```
そしてソースファイルはこちらです。
```Objective-C:TouchSimulator.xm
#import "TouchSimulator.h"
#import <dlfcn.h>

static void postEvent(IOHIDEventRef event);
static void execute();

static IOHIDEventRef parent = NULL;

void simulateTouch(TouchType type, float x, float y) {
    if (parent == NULL) {
        parent = IOHIDEventCreateDigitizerEvent(
            kCFAllocatorDefault, //allocator
            mach_absolute_time(), // timeStamp
            kIOHIDDigitizerTransducerTypeHand, //IOHIDDigitizerTransducerType
            0, // uint32_t index
            0, // uint32_t identity
            kIOHIDDigitizerEventTouch, // uint32_t eventMask
            0, // uint32_t buttonMask
            0.0, // IOHIDFloat x
            0.0, // IOHIDFloat y
            0.0, // IOHIDFloat z
            0.0, // IOHIDFloat tipPressure
            0.0, //IOHIDFloat barrelPressure
            0, // boolean range
            true, // boolean touch
            0// IOOptionBits options
        );
        IOHIDEventSetIntegerValue(parent, kIOHIDEventFieldDigitizerIsDisplayIntegrated, 1);
    }

    uint32_t isTouch = type == TOUCH_UP ? 0 : 1;
    IOHIDDigitizerEventMask eventMask = 0;
    if (type != TOUCH_UP && type != TOUCH_DOWN)
        eventMask |= kIOHIDDigitizerEventPosition;
    if (type == TOUCH_UP || type == TOUCH_DOWN)
        eventMask |= (kIOHIDDigitizerEventTouch | kIOHIDDigitizerEventRange);

    IOHIDEventRef child = IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault, mach_absolute_time(), 1, 3, eventMask, x, y, 0.0f, 0.0f, 0.0f, isTouch, isTouch, 0);
    IOHIDEventSetFloatValue(child, kIOHIDEventFieldDigitizerMajorRadius, 0.04f);
    IOHIDEventSetFloatValue(child, kIOHIDEventFieldDigitizerMinorRadius, 0.04f);
    IOHIDEventAppendEvent(parent, child);

    execute();
}

static UIWindow* getKeyWindow() {
    for (UIWindow *window in [UIApplication sharedApplication].windows) {
        if (window.isKeyWindow) {
            return window;
        }
    }
    return NULL;
}

static void postEvent(IOHIDEventRef event) {
    static IOHIDEventSystemClientRef ioSystemClient = nil;
    UIWindow* keyWindow = getKeyWindow();

    if (ioSystemClient == NULL) {
        ioSystemClient = IOHIDEventSystemClientCreate(kCFAllocatorDefault);
    }
    if (event != NULL && keyWindow != NULL) {
        uint32_t contextID = keyWindow._contextId;
        void *handle = dlopen("/System/Library/PrivateFrameworks/BackBoardServices.framework/BackBoardServices", RTLD_NOW);
        if (handle) {
            typedef void (* BKSHIDEventSetDigitizerInfoType)(IOHIDEventRef, uint32_t, uint8_t, uint8_t, CFStringRef, CFTimeInterval, float);
            BKSHIDEventSetDigitizerInfoType digitizer = (BKSHIDEventSetDigitizerInfoType)dlsym(handle, "BKSHIDEventSetDigitizerInfo");
            digitizer(event, contextID, false, false, NULL, 0, 0);
            [[UIApplication sharedApplication] _enqueueHIDEvent:event];
        }
    }
    IOHIDEventSystemClientDispatchEvent(ioSystemClient, event);
}

static void execute() {
    IOHIDEventSetIntegerValue(parent, kIOHIDEventFieldDigitizerTiltX, kIOHIDDigitizerTransducerTypeHand);
    IOHIDEventSetIntegerValue(parent, kIOHIDEventFieldDigitizerTiltY, 1);
    IOHIDEventSetIntegerValue(parent, kIOHIDEventFieldDigitizerAltitude, 1);
    postEvent(parent);
    CFRelease(parent);
    parent = NULL;
}
```
こんな感じになっています。
IOKitとBackBoardServicesという見慣れないフレームワークが出てきたので混乱している人も多いと思います。
簡単に説明すると、
IOKitは、ハードウェアとかカーネルとやりとりができるもので、
BackBoardServicesは、タッチなどのハードウェアからのイベントを処理したり、IOKitとユーザーランドとの橋渡しの役割をするものです。BackBoardServicesはbackboarddというプロセスでシステムに常駐しています。
タッチイベントを生成してからアプリに伝わるまでの流れとしては、
まず、IOKitでタッチイベントを生成します。そして生成したタッチイベントをBackboardServicesでどのWindowで受け取るかを、UIWindowの_contextIdをもとに決めます。最後にeventをUIApplicationに伝えることでタッチ処理を行うことができます。

次はサーバーから座標を受け取ってsimulateTouchを実行するところを見ていきます。

```Objective-C:Tweak.xm
#import "Position.h"
#import "TouchSimulator.h"
#import "WebSocketClient.h"

%ctor {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        WebSocketClient *client = [[WebSocketClient alloc] init];
        [client didReceive: ^(NSString *response) {
            Position *position = [[Position alloc] initWithString: response];
            switch (position.mode) {
                case BEGAN://began
                    simulateTouch(TOUCH_DOWN, position.x, position.y);
                    break;
                case CHANGED://changed
                    simulateTouch(TOUCH_MOVE, position.x, position.y);
                    break;
                case ENDED://ended
                    simulateTouch(TOUCH_UP, position.x, position.y);
                    break;
                case TAPPED:
                {
                    simulateTouch(TOUCH_DOWN, position.x, position.y);
                    simulateTouch(TOUCH_UP, position.x, position.y);
                    break;
                }
                default:
                    break;
            }
        }];
        [client setup:@"ws://192.168.0.33:8020"];
        [client connect];
    });
}
```
拡張子がxmと見慣れないですが、脱獄用のソフトウェアを開発する際に使われるLogosという言語の拡張子です。
LogosはObjective-CやObjective-C++の構文にプラスして、%ctorなどの独自の構文を追加したものとなっています。
%ctorはSpringBoardを起動した際、もしくは何らかのアプリを起動した際に実行されるものです(TweakのTargetによってことなる)
なので、このコードはなんらかのアプリが起動されてから5秒後にWebSocketサーバーに接続をして座標を受け取り、simulateTouchで受け取った座標をもとにタッチイベントを実行しているコードとなっています。
なぜ遅延処理をしているかというと、スプラッシュ画面(アプリを起動した際に出るやつ)が表示されている間にWebSocketの接続の処理を実行しても接続されないためです。

だいぶ端折りましたが、このようにして遠隔操作をすることができました！！

# まとめ
ソースコードはこちらからみれます。
https://github.com/Ryu0118/RemoteControl

# 参考
https://iphonedev.wiki/index.php/Backboardd

https://github.com/apple-oss-distributions/WebKit2/blob/main/UIProcess/_WKTouchEventGenerator.mm