Frida를 소개합니다! 멀티 플랫폼 후킹을 위한 가장 강력한 도구 😎

간만에 툴 소개를 좀 할까 합니다. 오늘 이야기드릴 툴은 Frida 입니다. 파이썬 기반의 라이브러리와 Command로 구성되어 있고 Native App에 대한 후킹을 통해 동적 분석을 진행할 수 있는 도구이죠.

What is Frida?

Frida는 JS Injection을 이용하여 Windows, macOS, Linux, iOS, Android, and QNX 기반의 네이티앱에 대해 후킹이 가능한 파이썬 라이브러리입니다. 대표적으로 iOS, Android 등 모바일 분석 때문에 알려져 있지만 다른 플랫폼에서도 사용이 가능하기 때문에 확장적인 면에서 좋습니다.

물론 Core 부분은 C와 Google V8 Engine으로 작성 되었지만 대다수 구현체는 python입니다. 또한 js, c, swift 등 여러 언어에 대한 API를 지원하니 입맛에 맞게 추가로 개발해서 사용하시면 좋습니다.

Installation

python package로 제공되고 있어 pip를 통해 설치가 가능합니다.

pip install frida
Collecting frida
  Downloading frida-10.5.8.tar.gz
Requirement already satisfied: colorama>=0.2.7 in /usr/local/lib/python2.7/dist-packages/colorama-0.3.9-py2.7.egg (from frida)
Collecting prompt-toolkit>=0.57 (from frida)
  Downloading prompt_toolkit-1.0.15-py2-none-any.whl (247kB)
    100% |████████████████████████████████| 256kB 1.2MB/s
Requirement already satisfied: pygments>=2.0.2 in /usr/lib/python2.7/dist-packages (from frida)
Requirement already satisfied: six>=1.9.0 in /usr/lib/python2.7/dist-packages (from prompt-toolkit>=0.57->frida)
Requirement already satisfied: wcwidth in /usr/local/lib/python2.7/dist-packages/wcwidth-0.1.7-py2.7.egg (from prompt-toolkit>=0.57->frida)
Building wheels for collected packages: frida
  Running setup.py bdist_wheel for frida ... \

Download & Setting frida server(android)

실제 사용을 위해선 frida를 분석할 pc에 설치한 이후에 각 디바이스에도 firda-server 설치가 필요합니다.

  • https://github.com/frida/frida/releases

릴리즈 페이지에서 frida server를 다운로드합니다. 여기서 frida-server는 각 플랫폼과 비트별로 버전이 있고 환경에 맞게 받아서 사용하시면 됩니다.

releases 페이지

안드로이드의 경우 .xz로 묶여있습니다. 풀어주세요!

ls
frida-server-10.5.8-android-arm.xz
unxz frida-server-10.5.8-android-arm.xz
ll
합계 45560
drwxr-xr-x  2 hahwul hahwul     4096  8월 31 22:08 ./
drwxr-xr-x 67 hahwul hahwul     4096  8월 31 21:33 ../
-rw-rw-r--  1 hahwul hahwul 46650120  8월 31 22:08 frida-server-10.5.8-android-arm

저는 편의를 위해 이름을 바꿨습니다. 다만 시간이 지나면 분명 frida 버전을 계속 올리시야 할테고 이 때 버전명이 없으면 보기 불편하기 떄문에 가급적이면 그냥 사용하시는 것을 더 추천드려요!

cp frida-server-10.5.8-android-arm frida-server

adb로 안드로이드 폰에 연결한 후 frida-server를 폰에 넣어 실행해줍니다.

adb connect 192.168.0.74
* daemon not running. starting it now on port 5037 *
* daemon started successfully *
connected to 192.168.0.74:5555

push로 넣어주고, 권한 설정 후. 실행해 줍니다. 물론 이 과정은 root 권한으로 진행이 필요합니다.

adb root
adb push frida-server /data/local/tmp
855 KB/s (46650120 bytes in 53.268s)
adb shell "chmod 777 /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &"

이제 frida-server가 실행된걸 확인해보면..

#(android) ps | grep server                         
drm       331   1     29452  1660  ffffffff b6f0b090 S /system/bin/drmserver
media     332   1     199940 4884  ffffffff b6eb6090 S /system/bin/mediaserver
media     370   1     42412  1380  ffffffff b6f79090 S /system/bin/dmbserver
system    881   366   2226688 110888 ffffffff b6ef79c8 S system_server
system    1315  1     7192   668   ffffffff b6ef0090 S /system/bin/tlc_server
system    1316  1     7192   664   ffffffff b6eec090 S /system/bin/tlc_server
radio     1683  366   1832928 16752 ffffffff b6ef79c8 S com.android.server.telecom
root      20932 20777 39852  28996 ffffffff b5dcff84 S ./frida-server

맨 아래 20932번으로 잘 돌아가고 있네요.

Frida command

frida cli는 frida-server와 연결하여 앱을 후킹하고 원하는 동작을 수행하는데 있어서 필수인 도구입니다. 옵션을 보면 아래와 같습니다.

Usage: frida [options] target

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -D ID, --device=ID    connect to device with the given ID
  -U, --usb             connect to USB device
  -R, --remote          connect to remote frida-server
  -H HOST, --host=HOST  connect to remote frida-server on HOST
  -f FILE, --file=FILE  spawn FILE
  -n NAME, --attach-name=NAME
                        attach to NAME
  -p PID, --attach-pid=PID
                        attach to PID
  --debug               enable the Node.js compatible script debugger
  --enable-jit          enable JIT
  -l SCRIPT, --load=SCRIPT
                        load SCRIPT
  -c CODESHARE_URI, --codeshare=CODESHARE_URI
                        load CODESHARE_URI
  -e CODE, --eval=CODE  evaluate CODE
  -q                    quiet mode (no prompt) and quit after -l and -e
  --no-pause            automatically start main thread after startup
  -o LOGFILE, --output=LOGFILE
                        output to log file

-D(Device) , -R(Remote) , -U(USB) , -H(Host) 옵션으로 타겟을 지정해주고, -P 옵션으로 원하는 pid를 후킹합니다.

frida -D 192.168.0.74:5555 -p 2011
     ____
    / _  |   Frida 10.5.8 - A world-class dynamic instrumentation framework
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at http://www.frida.re/docs/home/
Attaching...           

Attach된 이후부터는 JS 코드로 후킹이 가능해집니다. 다만, 매번 하나하나 코드를 써가면서 테스트 하기엔 어렵기 때문에 미리 Js 코드를 만들어두고 로드하시는게 편합니다. (function 으로 만들어 필요할 떄 불러쓰거나 익명함수로 바로 실행되도록 해서 결과를 확인한다는 둥.. 여러가지 방법이 있겠네요)

추가로 frida는 몇가지 명령을 별도로 지원합니다.

frida-ps

frida-ps는 frida-server를 통해 process list를 확인합니다. 직접 디바이스에 접근하지 않아도 pid를 미리 확인할 수 있어 빠르게 대상을 식별할 수 있습니다.

Usage

Usage: frida-ps [options]

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -D ID, --device=ID    connect to device with the given ID
  -U, --usb             connect to USB device
  -R, --remote          connect to remote frida-server
  -H HOST, --host=HOST  connect to remote frida-server on HOST
  -a, --applications    list only applications
  -i, --installed       include all installed applications

Example

frida-ps -D "192.168.0.74:5555"

  PID  Name
-----  --------------------------------------------------
23814  Com.sktelecom.minit
 8586  adbd
  363  adsprpcd
 1813  android.process.acore
  621  androidshmservice
  333  apaservice
  373  at_distributor
 2391  auditd
  365  bintvoutservice
  376  cnd
  380  cnss-daemon
 6669  com.ahnlab.v3mobilesecurity.soda
 3942  com.android.bluetooth

frida-trace

frida-trace는 함수 호출에 대해서 동적으로 추적해 줍니다. 예를들면 옵션을 주어 앱을 모니터링하고 있을 때 해당 앱에서 발생하는 function에 대해 기록하고 사용자에게 제공합니다.

Usage

Usage: frida-trace [options] target

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -D ID, --device=ID    connect to device with the given ID
  -U, --usb             connect to USB device
  -R, --remote          connect to remote frida-server
  -H HOST, --host=HOST  connect to remote frida-server on HOST
  -f FILE, --file=FILE  spawn FILE
  -n NAME, --attach-name=NAME
                        attach to NAME
  -p PID, --attach-pid=PID
                        attach to PID
  --debug               enable the Node.js compatible script debugger
  --enable-jit          enable JIT
  -I MODULE, --include-module=MODULE
                        include MODULE
  -X MODULE, --exclude-module=MODULE
                        exclude MODULE
  -i FUNCTION, --include=FUNCTION
                        include FUNCTION
  -x FUNCTION, --exclude=FUNCTION
                        exclude FUNCTION
  -a MODULE!OFFSET, --add=MODULE!OFFSET
                        add MODULE!OFFSET
  -T, --include-imports
                        include program's imports
  -t MODULE, --include-module-imports=MODULE
                        include MODULE imports
  -m OBJC_METHOD, --include-objc-method=OBJC_METHOD
                        include OBJC_METHOD

Example

frida-trace -D "192.168.0.74:5555" -i "Func 패턴"
Uploading data...
open: Auto-generated handler …/linker/open.js
open: Auto-generated handler …/libc.so/open.js

Frida in Console & Code

Frida를 사용하는 방법은 크게 2가지가 있습니다. 하나는 Console mode, 하나는 Code에서 라이브러리르 불러서 사용하는 형태입니다. 개인의 취향에 따라 다르지만 저의 경우는 Console를 애용하며, 프리다 실행 후 Interactive Shell에서 직접 Javascript 구문으로 후킹을 진행합니다.

#> frida -U "앱"

Android / iOS, PC 들 공통적으로 훅을 걸 포인트를 찾는게 가장 중요합니다. 코드를 볼 수 있는 환경이면 코드로 걸어서 바로 진입하고, 블랙박스 테스팅의 경우 리버싱이나 추가적인 분석으로 봐야할 함수의 위치를 찾아야하죠.

var hook = ObjC.classes.YourClass["- yourFunction"]
Interceptor.attach(hook.implementation, {onload(args){ console.log('gogogogo')  }});

그다음 Interceptor로 attach 하거나 replace 등으로 로직을 바꿔주시면 기존 동작과 다르게 앱을 동작시킬 수 있습니다. 여기서 발생하는 이벤트는 javascript의 이벤트와 동일하므로 onload, onleave 받아서 처리해주심되요. 프리다 공식홈에 잘 나와있으니 참고해주세요 :D

python에서 로드하는 경우

import frida, sys

def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)

jscode = """
Java.perform(function () {
    var MainActivity = Java.use('com.yourapp.MainActivity');
    MainActivity.onClick.implementation = function (v) {
        send('onClick');
        this.onClick(v);

        this.m.value = 0;
        this.n.value = 1;
        this.cnt.value = 999;

        // Log to the console that it's done, and we should have the flag!
        console.log('Done:' + JSON.stringify(this.cnt));
    };
});
"""

process = frida.get_usb_device().attach('com.your.app')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()

자세한 내용은 API Reference를 보시는게 좋을 것 같습니다.

Frida CodeShare

보통은 분석하는 대상 앱에 따라 frida 코드를 작성해서 테스트 하는데요, 서로 작성한 코드를 공유하는 사이트가 있습니다. frida에서 공식적으로 지원하는 웹 서비스고 이를 이용하면 미리 만들어진 코드를 통해 필요한 테스트를 조금 더 편하게 할 수 있습니다.

  • https://codeshare.frida.re/browse

Frida 코드 작성법(?)에 대해 감 잡기도 좋은 것 같구요. Frida 많이 쓰는 이유 중 하나가 SSL Unpinning 때문이기도 한데요, codeshare에 Pinning 우회 코드가 있습니다. 참고하셔서 필요한 부분은 수정해서 쓰시면 됩니다. (동작 방식은 키 스토어를 디바이스껄로 바라보도록 로직을 처리하는 형태입니다)

  • https://codeshare.frida.re/@pcipolloni/universal-android-ssl-pinning-bypass-with-frida/
setTimeout(function(){
    Java.perform(function (){
        console.log("");
        console.log("[.] Cert Pinning Bypass/Re-Pinning");

        var CertificateFactory = Java.use("java.security.cert.CertificateFactory");
        var FileInputStream = Java.use("java.io.FileInputStream");
        var BufferedInputStream = Java.use("java.io.BufferedInputStream");
        var X509Certificate = Java.use("java.security.cert.X509Certificate");
        var KeyStore = Java.use("java.security.KeyStore");
        var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");
        var SSLContext = Java.use("javax.net.ssl.SSLContext");

        // Load CAs from an InputStream
        console.log("[+] Loading our CA...")
        cf = CertificateFactory.getInstance("X.509");

        try {
            var fileInputStream = FileInputStream.$new("/data/local/tmp/cert-der.crt");
        }
        catch(err) {
            console.log("[o] " + err);
        }

        var bufferedInputStream = BufferedInputStream.$new(fileInputStream);
          var ca = cf.generateCertificate(bufferedInputStream);
        bufferedInputStream.close();

        var certInfo = Java.cast(ca, X509Certificate);
        console.log("[o] Our CA Info: " + certInfo.getSubjectDN());

        // Create a KeyStore containing our trusted CAs
        console.log("[+] Creating a KeyStore for our CA...");
        var keyStoreType = KeyStore.getDefaultType();
        var keyStore = KeyStore.getInstance(keyStoreType);
        keyStore.load(null, null);
        keyStore.setCertificateEntry("ca", ca);

        // Create a TrustManager that trusts the CAs in our KeyStore
        console.log("[+] Creating a TrustManager that trusts the CA in our KeyStore...");
        var tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        var tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
        tmf.init(keyStore);
        console.log("[+] Our TrustManager is ready...");

        console.log("[+] Hijacking SSLContext methods now...")
        console.log("[-] Waiting for the app to invoke SSLContext.init()...")

           SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").implementation = function(a,b,c) {
               console.log("[o] App invoked javax.net.ssl.SSLContext.init...");
               SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").call(this, a, tmf.getTrustManagers(), c);
               console.log("[+] SSLContext initialized with our custom TrustManager!");
           }
    });
},0);

기능에 따라 코드가 많이질수도 있는데, 다행히 Frida에서 codeshare를 바로 불러와 사용하는 기능을 제공합니다. 바로 --codeshare 옵션으로 업로더/이름 형태로 불러와 사용할 수 있습니다. 위의 코드로 예를들면 아래 형태로 바로 로드하여 실행이 가능하겠죠.

frida -U --codeshare pcipolloni/universal-android-ssl-pinning-bypass-with-frida

Troubleshot

설치 과정 중 몇가지 에러 포인트가 있어서 메모해둡니다.

Error - not executable: magic 7F4

Android에서 frida-server실행 시 “not executable: magic 7F4” 메시지 발생하는 경우 보통 아키텍쳐 문제입니다. readelf 등으로 파일 헤더를 확인해보시면 왜 실행할 수 없는 파일인지 아실 수 있을겁니다.

readelf --file-header --arch-specific frida-server

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x7beb0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          46648520 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         25
  Section header string table index: 24

제 케이스를 보면 type이 DYN이네요. 초기에 x86으로 잘못 받아서 에러가 났던것입니다. frida-server-10.5.8-android-x86_64 arm 버전으로 받아서 실행하면 잘 됩니다.

adbd cannot run as root in production builds

에러 내용과 같이 adbd에서 root run을 지원하지 않는 경우입니다. 이런 경우 adb를 root run이 가능하게 다시 build 하거나 그냥 직접 adb shell로 접근하여 su를 통해 root로 변경 후 작업하시면 됩니다.

Reference

  • https://www.frida.re/docs/examples/android/