1. Frida

Inject JavaScript to explore native apps on Windows, macOS, Linux, iOS, Android, and QNX.

[출처] https://www.frida.re/


Frida는 자바스크립트를 이용하여 Windows, macOS, Linux, iOS, Android와 QNX와 같은 다양한 플랫폼에서 후킹을 할 수 있는 플랫폼이다. 파이썬 틀에 인젝션할 코드를 자바스크립트로 작성하여 파이썬으로 실행 할 수 있다. 



2. Frida Hooking

iOS Hooking#1 에서 분석한 동일한 앱을 대상으로 진행을 하였으므로 앱 분석은 생략한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# -*- conding: utf-8 -*-
import frida
import sys 
 
PACKAGE_NAME = "kr.history"
 
#send실행 시 출력할 format정의
def on_message(message, data):
    try:
        if message:
            print("[JSBACH] {0}".format(message["payload"]))
    except Exception as e:
        print(message)
        print(e)
 
 
def do_hook():
 
    hook = ""
    //Objective-C가 실행 가능한 환경인지 검사
    if(ObjC.available){
        //해당 attach된 프로세스의 메모리에 올라가있는 클래스들을 가져올 수있고,
        //아래 for문은 타겟 클래스가 존재하는지 검사했다.
        for(var className in ObjC.classes){
            if(className == "Game2ViewController"){
                send("Found our target class : " + className);
            }
        }
    
        //Hooking을 진행할 메서드 객체를 가져온다.
        var hook_method = ObjC.classes.Game2ViewController["- recognizeAnswer"];
        send("print hook_method : " + hook_method);
        
        Interceptor.attach(hook_method.implementation, {
        
            //onEnter는 후킹함수 진입 시 실행되며, args[0]에는 self객체가
            //args[1]에는 selector객체가 들어있어 접근 가능하며
            //args[2]에는 해당 함수의 매개변수들이 들어있다.
            //매개변수를 변경하고 싶다면 이곳에서 변경한다.
            onEnter: function(args){
                
                var receiver = new ObjC.Object(args[0]);
                send("Target class : " + receiver.$className);
                send("Target superclass : " + receiver.$superClass.$className);
                var sel = ObjC.selectorAsString(args[1]);
                send("typeof sel = " + typeof sel);
                send("Hooked the target method : " + sel);
            },
            //onLeave는 함수 종료전 처리를 할 수 있다.
            //retVal에는 원래의 return값이 들어있고, return값을 변경하고 싶다면
            //이곳에서 변경한다.
            onLeave: function(retVal){
                //오답 시 리턴 값
                var wrong   = -1;
                //정답 시 리턴 값
                var correct = 1; 
                //retVal은 Object객체이며, int값으로 사용하기 위해 toInt32()를 사용
                var orig_rtn = retVal.toInt32();
                if(-1 == orig_rtn){
                    send("answer is Wrong!! : " + orig_rtn);
                    send("answer is replaced!!");
                    //값을 변경하기위해 replace()를 사용하여 변경함
                    retVal.replace(correct);
                }
                else{
                    send("answer is Correct!! : " + orig_rtn);
                }
            }
        });
    }
    //Objective-C 실행 환경이 아닌 경우 로그 출력
    else{
        console.log("Objective-C Runtime is not available!");
    }
    """
 
    return hook
 
if __name__ == '__main__':
 
    try:
        #연결할 단말을 찾는다.
        device = frida.get_device_manager().enumerate_devices()[-1]
 
        #타겟으로 할 앱의 패키지명으로 단말에서 실행되고 있는 pid를 가져온다.
        pid = device.spawn([PACKAGE_NAME])
        print("[JSBACH] {} is starting. (pid : {})".format(PACKAGE_NAME, pid))
 
        #위에서 얻어온 pid에 attach한다.
        session = device.attach(pid)
        device.resume(pid)
 
        #후킹코드를 injection한다.
        script = session.create_script(do_hook())
        script.on('message', on_message)
        script.load()
        sys.stdin.read()
    except KeyboardInterrupt:
        sys.exit(0)



코드에 대한 설명은 주석으로 대체합니다.


3. 결과





4. 코드 작성하며 발생했던 사소한 문제점들

4-1) frida client / server간 버전 충돌 문제

frida --version / frida-server --version 으로 확인한 메이저버전이 다를 경우 frida가 제대로 동작하지 않는다.


4-2) 32bit 단말 문제

실습에 활용한 단말은 iPhone 5로 32bit 단말이였는데, Frida최신버전(9.1.27)버전에서 제대로 훜이 걸리지않음

-> 8.2.2버전으로 낮추어 설치하여 정상적으로 동작 (pip install frida==8.2.2)


4-3) retVal 값 format

onLeave에있는 retVal값을 toString()을 이용해 사용하면 헥사 값이 나온다. (위 예제에서는 오답인 경우 0xffffffff, 정답인 경우 0x1) 따라서 toInt32()함수를 이용해서 int로만들어 주었다.

0xffffffff를 parseInt()를 사용하여 출력하면 unsigned int형의 최대값인 4294967295가 출력되었다.


4-4) onEnter

onEnter에서 args에 접근하여 receiver와 receiver의 superclass를 출력할 때, 많은 예제코드에서 

$className이 생략되어있었는데, 타입 변환 에러가 나면서 제대로 출력되지 않았으며, frida API를 참조해서 

$className을 추가하여 해결함

[참조] https://www.frida.re/docs/javascript-api/#objc


4. 후기

Logos와 비교했을 때, 후킹 코드의 빌드나 설치가 별도로 필요 없어서 가볍다는 느낌이 들었다.

python과 JavaScript로 iOS후킹 코드를 짤 수 있다는 게 신기했고,

탈옥상태가 아닌 폰에서도 후킹이 가능하다는 점도 신기했다.

'Study > iOS' 카테고리의 다른 글

iOS Hooking#3(Cycript)  (3) 2017.05.06
iOS Hooking#1(Logos)  (1) 2017.04.18
2장 iOS 해킹 기초 (1)  (0) 2016.06.18
자주쓰는 데이터형 변환  (0) 2016.06.01
[iOS/GCC] __attribute__((constructor)) / __attribute__((desstructor))  (0) 2016.05.13

1. Logos

Logos is a component of the Theos development suite that allows method hooking code to be written easily and clearly, using a set of special preprocessor directives.


-[출처]http://iphonedevwiki.net/index.php/Logos


로고스는 Theos컴포넌트로 로고스를 이용하면 iOS 메소드 후킹 코드를 쉽고 깔끔하게 작성할 수 있다.

로고스를 이용한 후킹을 실습하기 위해 퀴즈앱을 대상으로 진행해보았다.


2. App Logic

2-1) 앱 실행 화면

앱은 퀴즈와 함께 해당 퀴즈 정답의 글자 수가 표시되고, 아래 글자를 클릭하여 정답을 맞추는 형식을 갖추고 있다.


2-2) iDA 정적 분석


Clutch를 이용하여 복호화한 Binary를 iDA로 열어 코드를 분석하면 위와 같은 코드를 발견 할 수 있는데, recognizeAnswer라는 메소드의 결과 값에 따라 congratulationViewVisible여부를 결정 하는 것을 알 수 있다.


recognizeAnswer메소드의 내용은 다음과 같다.


위 코드에서는 조금 잘렸는데, v2는 Game2ViewController의 self객체이고, answer_nospace의 길이가 0이 아니면 answer_nospace의 길이만큼 while문을 돌면서 userAnswer와 answer_nospace를 한글자씩 비교한다.

그 후 모두 매치하면 v15를 1로 만들고 매치하지 않으면 break한 후 v15를 -1로 만드게 되고 이 v15값을 리턴한다. 즉 유저의 정답과 문제의 정답을 비교하여 정답인 경우 1을 리턴하고, 오답인 경우 -1을 리턴하는 메서드임을 알 수 있다. 


2-3) Class-dump

2-2에서 확인한 Game2ViewController의 멤버변수나 메서드들을 Class-dump로 확인해 보았고, 우리가 주시해야하는 것들 부분만 발췌하였다.

answer_nospace는 앱에서 가지고있는 (분석한 결과 앱내 DB에서 조회해오는 값) 정답에 해당하는 변수이고, userAnswer는 유저가 입력한 답안 변수이다.



recognizeAnswer메서드는 멤버변수 값들로만 리턴 값을 정할 수 있으므로 특별한 Input값은 필요하지 않은 형태로 존재함을 확인 할 수있다.



3. Logos Hooking

위 분석 결과를 바탕으로 recognizeAnswer를 후킹하여 어떤 답안이 입력되든 정답처리가 될 수 있도록 Logos를이용한 후킹코드를 작성해 보았다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
  
@interface Game2ViewController : UIViewController{
  - (int)recognizeAnswer;
@end
%hook Game2ViewController
- (int)recognizeAnswer{
    int ret = %orig;
    int correct = 1;
    NSLog(@"[JSBACH]recognizeAnswer is Hooked\n"); 
    if(-1 == ret){
         NSLog(@"[JSBACH]answer is Wrong!! : %d", ret);
    }    
    else{
         NSLog(@"[JSBACH]answer is Correct!! : %d", ret);
    }
 
    return correct;
}
%end



@interface Game2ViewController : UIViewController{

}


- (int)recognizeAnswer;

@end


후킹 타겟으로 하는 클래스/인터페이스와 해당 메서드의 form을 정의해준다.

우리가 후킹할 Game2ViewController는 UIViewController의 sub이므로 Game2ViewController : UIViewController부분이 있고, class-dump의 결과에서 확인했던 것과 같이 - (int)recognizeAnswer 메서드 형태로 정의해준다.


%hook Game2ViewController 후킹할 클래스/인터페이스를 지정하고 %end로 후킹코드의 끝을 내준다.

%orig는 원래 메서드의 리턴값을 가져올 수 있으며, argument가 존재하는 경우에는 %orig(arg1, arg2, ...)형식으로 호출 가능하다.


여기서는 본래의 리턴값을 가져와서 정답/오답여부를 로그로 표출하고, 무조건 정답처리 할 수 있도록 코딩하였다.



3. 후킹 결과



정상적으로 후킹 되어 로깅이 되고있는 모습이다.



원래 정답은 비파형 / 세형이지만 아무 값이나 오답을 입력한 후 테스트 하였고 정답처리가 됨을 확인하였다. :)



4. 후기

Logos를 이용해서 트윅제작, 시스템콜 후킹, UI버튼을 추가하는 등의 다양한 여러작업이 가능하다고 들었는데, 천천히 하나씩 해보고 싶다. 우선 다음 게시글은 Frida를 이용해서 똑같은 행위를 하는 후킹코드를 작성 해볼 예정이다.





'Study > iOS' 카테고리의 다른 글

iOS Hooking#3(Cycript)  (3) 2017.05.06
iOS Hooking#2(Frida)  (0) 2017.04.19
2장 iOS 해킹 기초 (1)  (0) 2016.06.18
자주쓰는 데이터형 변환  (0) 2016.06.01
[iOS/GCC] __attribute__((constructor)) / __attribute__((desstructor))  (0) 2016.05.13

1. ptrace 

 ptrace는 여러 유닉스와 유닉스 계열 운영 체제에서 발견되는 시스템 호출이다. ptrace(이 이름은 "process trace"의 축약형이다)를 통해 컨트롤러가 대상의 내부 상태를 조사하고 조작하게 함으로써, 한 프로세스가 다른 프로세스를 제어할 수 있다. ptrace는 디버거와 다른 코드 분석, 특히 소프트웨어 개발을 도와주는 도구들에서 사용된다.

[출처] 위키백과 https://ko.wikipedia.org/wiki/Ptrace


2. 목적

 본래의 목적은 안드로이드 LD_PRELOAD 환경 변수에 라이브러리명을 인젝션하여 특정 함수를 후킹하는 것

2-1) LD_PRELOAD 

 프로세스 실행 전 라이브러리 로딩 과정에서 해당 환경변수에 추가되어있는 라이브러리를 우선적으로 로딩하게 만드는 환경변수

2-2) 메인아이디어 

 zygote를 kill하게되면 init프로세스에서 zygote프로세스를 재생성하며, 이때 LD_PRELOAD환경 변수가 재로드되고, 이때 값을 조작하여 원하는 라이브러리를 인젝션한다.


2-3) 목적 달성 과정

 - zygote process kill (성공)

 - new zygote process의 pid를 알아냄 (성공)

 - new zygote process에서 실행하는 execve 실행 시점에서 환경변수 값 조작 (실패)

 - 후킹 여부 확인 (실패)


3. aarch32 vs aarch64

3-1) register갯수가 다름

 - register갯수가 대폭 추가되면서 ptrace를 이용하여 system call을 이해하기 위해 사용되는 register번호 또한 달라졌으며, 그에 따라 코드에서 사용하는 구조체 및 상수 또한 달라졌다.

 

aarch32 

aarch64 

system call number표시 reg 

 8

 IP reg

ARM_ip 

 12

 return 값 reg

reg표시 structure

pt_regs

 user_regs_struct

 syscall number 참조

 리눅스 syscall number와 동일

ndk-bundle\include\asm-generic\unistd.h


 - aarch64 레지스터 설명

r30 (LR): The Link Register, is used as the subroutine link register (LR) and stores the return address when Branch with Link operations are performed.

r29 (FP): The Frame Pointer

r19…r28: Callee-saved registers

r18: The Platform Register, if needed; otherwise a temporary register.

r17 (IP1): The second intra-procedure-call temporary register (can be used by call veneers and PLT code); at other times may be used as a temporary register.

r16 (IP0): The first intra-procedure-call scratch register (can be used by call veneers and PLT code); at other times may be used as a temporary register.

r9…r15: Temporary registers

r8: Indirect result location register

r0…r7: Parameter/result registers


3-2) zygote 프로세스

 - aarch32에서는 zygote process밖에 존재 하지 않지만, aarch64에서는 zygote / zygote64 process가 존재함 

 - aarch64에서 zygote 프로세스는 두 개지만 zygote process를 kill하면 zygote64 또한 재 실행되는 것으로 보임


3-3) zygote 프로세스 kill 후

 - 새 프로세스 생성 전 aarch32에서는 stat64 system call을 사용하여 실행 바이너리(app_process)의 정보를 얻어 오지만, aarch64에서는 fstat64 system call을 사용하여 실행 바이너리(app_process32 / app_process64)의 정보를 얻어오며, aarch64의 app_process 바이너리는 app_process64의 심볼릭 링크이다.

 - aarch32에서는 fork system call로 zygote process를 생성하지만 aarch64에서는 clone system call을 이용

 - 개인적으로 fork / clone system call의 가장 큰 차이점은 프로세스 복사(생성) 이 후 실행 할 바이너리를 경로로 넘겨주느냐(fork), file descriptor(clone)의 차이가 가장 큰 외적 차이로 보이며, 조사를 통한 내적 차이는 parent process로부터 메모리 스택을 공유(clone) / 비공유(fork)라고 생각하고, 전반적으로 clone의 system call이 fork에 비해 비교적 비용이 낮은 것으로 설명이 되어있었다.


3-4) execve system call 탐지 가능 여부

이 부분은 aarch64에서도 아마도(?) 탐지가 가능할 것이나 내 능력/자료가 부족한 것으로 생각된다.

 - ptrace를 사용하여 레지스터 값들을 조사할 때 실행되는 system call number에 execve system call에 해당하는 220번이 탐지 되지 않음

 - strace를 이용하여 값을 조사했을 때는 execve system call이 실행되는 것이 확인됨


4. aarch64에서 ptrace사용

4-1) 레지스터 값을 가져오기 위한 구조체 선언

1
2
3
4
5
6
7
8
9
10
//#include <linux/PTRACE.h>
//#include <sys/PTRACE.h>
 
//aarch64에서 달라진 구조체
struct user_regs_struct gregs;
 
//aarch64에서 추가로 필요한 부분
struct iovec iovecs;
iovecs.iov_base = &gregs;
iovecs.iov_len  = sizeof(gregs);


4-2) ptrace attatch

1
2
3
4
5
6
7
8
// step 1. init프로세스에 PTRACE attatch 
ret = PTRACE(PTRACE_ATTACH, INIT_PID, (void *)10);
if(ERROR == ret){
    printf("[ERROR] PTRACE_ATTACH failed return = %d\n", ret);        
}
else{
    printf("[INFO] PTRACE_ATTACH SUCCESS\n");    
}



4-3) 레지스터 값 얻어오기

1
2
3
4
5
6
7
8
9
10
11
// cpu 레지스터 획득
ret = PTRACE(PTRACE_GETREGSET, INIT_PID, (void *)NT_PRSTATUS, &iovecs);
        
if ( ret < 0 ) {
    printf("[ERROR] PTRACE_GETREGSET ERROR : %d\n", ret);
            
}       
else{
    printf("[INFO] PTRACE_GETREGSET SUCCESS : %d\n", ret);                
}








4-4) system call 탐지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define SYSCALL_REGISTER(r)     r.regs[8]
#define RESULT_REGISTER(r)      r.regs[0]
#define IP_REGISTER(r)          r.regs[12]
 
//gregs.regs[28]이 0이 무엇을 뜻하는 지는 알수 없으나,
//여러 번 반복해보는 과정을 통해 0인 경우일 때가 new pid가 리턴되는 것을 확인함
//그 중에서도 리턴 값이 정상인 것과 비정상인 값이 있었는데,
//정상 값을 찾기 위해 maximum pid값을 지정함(cat /proc/sys/kernel/pid_max -> 32768)
if ( 220 == SYSCALL_REGISTER(gregs) && 
      40 == IP_REGISTER(gregs)        && 
       0 == gregs.regs[28]             &&
   32768 >= RESULT_REGISTER(gregs) ) {
            printf("[*] Found a new zygote64 process: %ld\n", RESULT_REGISTER(gregs));
            
            new_zygote_pid = RESULT_REGISTER(gregs);
}



4-5) ptrace detach

1
2
3
4
5
6
7
//ptarce detach
if(PTRACE(PTRACE_DETACH, INIT_PID, (void*)10< 0) {
        printf("[ERROR] PTRACE_DETACH error\n");
}
else{
        printf("[INFO] PTRACE_DETACH SUCCESS\n");
}



'Study > 안드로이드' 카테고리의 다른 글

진술서전송앱  (0) 2022.11.03
JDB이용하여 앱 동적 분석하기  (0) 2017.02.13
Drozer 기본 명령어  (0) 2017.02.09
안드로이드 재서명 하기  (2) 2016.05.08
안드로이드 apk서명  (0) 2016.05.02

+ Recent posts