Hook iOS applications using Frida

2018-12-31

Frida를 이용한 iOS 어플리케이션 후킹에 대해 알아보도록 한다.

Frida

Frida는 Windows, macOS, GNU/Linux, iOS, Android, QNX 플랫폼 위의 다양한 어플리케이션에 자바스크립트 코드 조각들을 삽입할 수 있도록 도와주는 도구이다. Frida API를 활용해서 다양한 기능들을 사용할 수 있도록 만들어져 있다. 주로 어떠한 어플리케이션의 API를 추적(trace)하거나, 이미 개발된 어플리케이션에 로깅 함수를 삽입(inject)하여 번거로운 빌드 과정 없이도 테스트를 진행할 수 있도록 하는 데 Frida를 사용할 수 있다.

Firda의 core 부분은 C로 작성되었다. Frida는 동작 시 구글 V8 엔진을 특정 프로세스에 삽입하여, JavaScript가 해당 프로세스 메모리의 모든 부분에 엑세스하고, 함수를 후킹하며 임의의 함수를 호출하도록 실행될 수 있게 돕는다.

본 포스팅에서는 Frida를 이용하여 iOS 어플리케이션에 다양한 조작을 하는 과정을 보일것이다. 본 예제는 Electra로 탈옥된 iOS 11.2.6 버전의 iPhone 8 기기에서 적용하였다.

Frida 설치 및 명령어 사용

macOS에서 파이썬을 통해 Frida API를 활용하기 위해서, pip를 이용해서 Frida를 설치한다.

$ pip install frida-tools

만약 pip에서 권한 오류가 발생 할 경우에는 다음 경로의 프로그램을 실행한다.

$ /Applications/Python\ 3.7/Install\ Certificates.command

iPhone에도 Frida를 설치 해 놓아야 한다. Cydia를 설치한 뒤, https://build.frida.re를 소스에 추가하면 Frida 패키지를 내려받을 수 있으며, 이를 설치하면 된다.

Frida를 실행하는 방법은 크게 두 가지가 있는데, USB를 이용하는 방법과 remote로 이용하는 방법이다.

USB로 Frida를 이용하기 위해서는, 먼저 USB 케이블로 휴대폰과 Mac을 연결한다. 그 뒤 -U 옵션을 주어서 frida 도구를 실행하면 된다.

cragy0516-MacBookPro:~ cragy0516$ frida-ps -U
 PID  Name
----  --------------------------------------------------------
1776  Cydia
 189  Mail
​```

별도의 설정 없이도 가능하므로 간단하지만, USB 연결이 불가능할 경우가 존재한다. 그럴때는 휴대폰에서 특정 포트로 서버를 열어두고 remote로 접근하면 된다.

iPhone:~ root# frida-server --listen=192.168.0.39:27042
2018-12-07 19:06:43.230 frida-server[2025:304354] Frida: Unable to check in with launchd: are we running standalone?

오류메시지가 뜨는것을 확인했으나 작동에는 이상이 없다.

cragy0516-MacBookPro:~ cragy0516$ frida-ps --host=192.168.0.39:27042
 PID  Name
----  --------------------------------------------------------
1776  Cydia
 189  Mail

Mac으로 돌아와서 --host= 옵션으로 접속할 수 있다.

Frida API 활용

Frida API를 활용하면 함수를 후킹하여 반환 값을 변조하거나, 심지어는 자신이 정의한 새로운 함수를 호출하도록 만들 수 있다.

시작하기

본 예제에서는 디바이스를 USB로 연결한 상태임을 가정한다. device_manager를 호출한 뒤, devices중 마지막을 가져온다. 마지막으로 연결된 USB 디바이스를 사용 할 것이기 때문이다. 그 뒤 PACKAGE NAME을 기반으로 새로운 프로세스를 spawn한다. 그 뒤 do_hook() 함수를 스크립트로 등록하고, logging을 위한 on_message 함수를 등록한다. 이에 대한 예시 코드는 아래와 같다.

import frida
import sys

def on_message(message, data):
    try:
        if message:
            print("[log] {0}".format(message["payload"]))
    except Exception as e:
        print(message)
        print(e)

def do_hook():
    hook = """
    if(ObjC.available) {
        send("Ready to frida!");
    } else {
        console.log("Objective-C Runtime is not available!");
    }
    """

    return hook

if __name__ == '__main__' :
    PACKAGE_NAME = "com.xxx.yyy"
    try :
        print ("[log] devices info : {}".format(frida.get_device_manager().enumerate_devices()))
        device = frida.get_device_manager().enumerate_devices()[-1]

        pid = device.spawn([PACKAGE_NAME])
        print ("[log] {} is starting. (pid : {})".format(PACKAGE_NAME, pid))

        session = device.attach(pid)
        device.resume(pid)

        script = session.create_script(do_hook())
        script.on('message', on_message)
        script.load()
        sys.stdin.read()
    except KeyboardInterrupt:
        sys.exit(0)

이에 대한 출력 결과는 아래와 같다.

$ python3 my_hook.py
[log] devices info : [Device(id="local", name="Local System", type='local'), Device(id="tcp", name="Local TCP", type='remote'), Device(id="device_id_is_here", name="iPhone", type='usb')]
[log] com.xxx.yyy is starting. (pid : 5675)
[log] Ready to frida!

함수 반환값 변조하기

함수를 후킹하기 위해서는 클래스 이름메소드 이름을 알아야 한다. IDA, hopper등의 바이너리 분석 도구를 활용하면 클래스 이름을 알아낼 수 있는데, 해당 클래스가 가지고 있는 모든 메소드를 알아내는 간단한 방법은 다음과 같다. ObjC.classes.ColorChecker는 ColorChecker 클래스를 나타내며, $ownMethods는 부모 클래스에는 없으며 자신만이 가지고 있는 고유한 메소드의 집합을 나타낸다.

def do_hook():
    hook = """
    if(ObjC.available) {
        var class_checker = ObjC.classes.ColorChecker;
        var methods_checker = class_checker.$ownMethods;
        methods_checker.forEach(function(m) {
            send(m);
        });
    } else {
        console.log("Objective-C Runtime is not available!");
    }
    """

    return hook

수정된 do_hook()메소드의 출력 결과는 다음과 같다.

$ python3 my_hook_2.py
[log] com.xxx.yyy is starting. (pid : 5690)
[log] + initialize
[log] - isSuperUser
[log] - isColorCorrect:
[log] - isColorChanged
[log] - isDirectory
[log] - isApplication

이제 함수를 후킹하여 함수의 반환 값을 조작 해 보도록 하자. Interceptor를 활용하면 함수를 후킹하여 반환 값을 변조하거나, 또는 새로운 사용자 정의 함수로 바꿔치기하거나, 새로운 함수를 등록할수도 있다. do_hook() 메소드를 아래와 같이 수정한다.

def do_hook():
    hook = """
    if(ObjC.available) {
        var class_checker = ObjC.classes.ColorChecker;
        var methods_checker = class_checker.$ownMethods;
        var isApplication = class_checker['- isApplication'];

        Interceptor.attach(isApplication.implementation, {
            onEnter: function(args) {
                var target = new ObjC.Object(args[0]);
                var sel = ObjC.selectorAsString(args[1]);
                send("Target class : " + target.$className);
                send("Target selector : " + sel);
            },
            onLeave: function(retVal) {
                send("Old return : " + retVal);
                retVal.replace("0");
                send("New return : " + retVal);
            }
        });
    } else {
        console.log("Objective-C Runtime is not available!");
    }
    """

    return hook

isApplication은 사용자의 휴대포에 특정한 어플리케이션이 설치되어 있는지를 감지하여, 설치되어 있다면 True, 설치되어 있지 않다면 False를 반환하는 메소드이다. 현재 리스트 안의 어플리케이션을 감지하여 True (1)을 반환하였는데, Interceptor에 의해 반환 값이 False (0)으로 변경되었다. 이에 대한 출력은 아래와 같다.

$ python3 my_hook_3.py
[log] com.xxx.yyy is starting. (pid : 5702)
[log] Target class : ColorChecker
[log] Target selector : isApplication
[log] Old return : 0x1
[log] New return : 0x0

사용자 정의 함수 호출

Frida API를 활용하면 또한 실제 함수를 호출하는 대신, 사용자 정의 함수를 호출하도록 할 수도 있다. 가장 간단한 방법으로 malloc 함수를 후킹하여 할당하는 바이트와 할당 위치를 구하는 방법을 알아보자.

def do_hook():
    hook = """
    if(ObjC.available) {
        var mallocPtr = Module.findExportByName('libsystem_c.dylib', 'malloc');
        var malloc = new NativeFunction(mallocPtr, 'pointer', ['int']);
        Interceptor.replace(mallocPtr, new NativeCallback(function (size) {
        var location = malloc(size)
        send("Allocated " + size + " bytes in " + location);
        return location;
    }, 'pointer', ['int']));

    } else {
        console.log("Objective-C Runtime is not available!");
    }
    """

    return hook

mallocPtr 변수는 Module.findExportByName을 활용하여 실제 라이브러리에서 이름을 가지고 malloc 함수의 포인터를 얻어내는 과정이다. 그 뒤 malloc 변수는 NativeFunction으로 선언하는데, mallocPtr 위치의 함수를 pointer 반환 값에 int인자 하나를 갖는다. 그 뒤에 replace를 이용하여 mallocPtr을 새로운 NativeCallback으로 교체한다. 기존의 malloc에 들어간 size를 가져오고, 실제 할당되는 위치를 로그로 남길 수 있도록 구현하였다. 이에 대한 출력 결과는 아래와 같다.

$ python3 my_hook_4.py
[log] Allocated 29 bytes in 0x1d4032680
[log] Allocated 16 bytes in 0x1d4016220
[log] Allocated 177 bytes in 0x1d41744c0
...
[log] Allocated 16 bytes in 0x1d4016260
[log] Allocated 53 bytes in 0x1d4274d00
[log] Allocated 16 bytes in 0x1d4016370
[log] Allocated 69 bytes in 0x1d408d070
[log] Allocated 16 bytes in 0x1d4016230
...
[log] Allocated 105 bytes in 0x1d40d4ac0
[log] Allocated 16 bytes in 0x1d40164b0
[log] Allocated 105 bytes in 0x1d40d4ba0

이상으로 Firda가 제공하는 Javascript API를 활용하여 다양한 조작을 해 보았다. 더 많은 정보는 공식 홈페이지에서 제공하는 API Reference를 참조하면 된다.