지난번 iOS Hooking관련 포스팅을 진행하면서 Frida를 소개해드린 바 있습니다.

[링크] http://bachs.tistory.com/entry/iOS-Hooking2Frida?category=892887


전에 언급했던 것처럼 Frida는 iOS뿐만아니라 Android와 Windows에서도 활용할 수 있는데요, 오늘은 Windows에서 활용하는 방법에 대해서 

포스팅을 해보려고 합니다. 예제는 코드엔진의 Basic level03 문제를 이용하였습니다.

[링크] http://codeengn.com/challenges/basic/03


1. 분석

예제 프로그램을 실행하면, 영어인듯 하지만 영어는 아닌것으로 보이는 알럿이 뜹니다.


 

여기서 취소를 누르면 그대로 프로그램을 종료되고, 확인을 누르면 진행이 됩니다.





위의 알럿창에서 뭐라고 이야기했는지 정확히는 모르지만 실행결과를 보니 Regcode를 입력해서 인증을 하는 프로그램인듯 합니다.

더불어 이 예제의 목적은 Regcode를 모르는 상태에서 이 프로그램을 크래킹해내는 것이겠죠.


여기까지를 보고 예상해볼 수 있는 점은 입력 기대값이 있고, 입력값과 비교해서 성공/실패여부를 반환할 것이라는걸 알 수 있습니다.

그리고 이 예제 프로그램이 통신을 하지 않는 것으로 보이니 파일안에 입력 기대값이 존재하거나 로컬에 존재하는 어떤 파일안에 존재할 가능성 등을 

생각해 볼 수 있습니다.  


자세한 건 IDA로 열어서 imports subview를 한번 봅시다.



보다시피 MSVBVM50이라는 라이브러리에서만 참조하는 것으로 보이네요. 검색을 해보면 MSVBVM50.dll은 Visual Basic으로 개발된 프로그램이

참조하는 dll이라는 것을 알 수 있습니다. 또한 목록에서 볼때는 일단 fopen같은 함수가 보이지 않으니, 입력 기대값이 실행 바이너리 안에 존재할 가능성이

더 높아졌네요.


입력값과 입력기대값을 비교하기위해 문자열 비교함수를 사용했을 가능성이 크니 해당 함수를 기점으로 분석 포인트를 잡는 것이 좋을 것같습니다.

import 함수 목록 중 __vbaStrCmp를 포인트로 잡아 보겠습니다.



__vbaStrCmp 함수의 레퍼런스를 찾아보니 두 군데가 나오고 있습니다. 위의 화면은 두 곳 중 더 상위주소에 있는 곳으로 이동한 화면입니다.

__vbaStrCmp 함수를 호출하기전에 ebp-58과 "2G83G35Hs2"라는 문자열을 함수의 인자로 사용하기 위해 push해주고 있는 모습입니다.

이 것을 보고  ebp-58에 우리가 입력한 입력값이 존재하고 "2G83G35Hs2"가 입력 기대값임을 알 수 있습니다.



오, 기대 입력 값은 잘 찾은거 같네요! 하지만 이번에는 Windows후킹에대해 포스팅을 하는 거니까 후킹으로 풀어봅시다.

일단, 우리가 후킹을 걸 메서드는 MSVBVM50.dll 의 __vbaStrCmp()이고, 리턴 값을 조작해주면 될거 같아요.


2. 후킹

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
from __future__ import print_function
import frida
import sys
 
def on_message(message, data):
    print("[%s] => %s" % (message, data))
 
def main(target_process):
    session = frida.attach(target_process)
    #print([x.name for x in session.enumerate_modules()])
  
    script = session.create_script("""
    var baseAddr = Module.findBaseAddress('MSVBVM50.dll');
    console.log('MSVBVM50.dll baseAddr: ' + baseAddr);
    //"use strict";
    const __vbaStrCmp = Module.findExportByName("MSVBVM50.dll", "__vbaStrCmp");
    Interceptor.attach(__vbaStrCmp, {
        onEnter: function (args) {
            console.log('===============================================================================');
            console.log('[+] Called __vbaStrCmp!! [' + __vbaStrCmp + ']');
            console.log('[+] args[0] = [' + args[0] + ']');
            dumpAddr('args[0]', args[0], 0x16); 
            console.log('[+] args[1] = [' + args[1] + ']');
            dumpAddr('args[1]', args[1], 0x16);
        },
        // When function is finished
        onLeave: function (retval) {
            console.log('===============================================================================');
            
            /*
                It doesn't work !
            console.log('[+] (Origin) Returned from __vbaStrCmp: ' + typeof(retval));
            console.log('[+] (Origin) Returned from __vbaStrCmp: ' + retval);
            retval = 0;
            console.log('[+] (forgery) Returned from __vbaStrCmp: ' + typeof(retval));
            console.log('[+] (forgery) Returned from __vbaStrCmp: ' + retval);
            */

            this.context.eax = 0x0;
            console.log('Context information:');
            console.log('Context  : ' + JSON.stringify(this.context));
            //console.log('Return   : ' + this.returnAddress);
            //console.log('ThreadId : ' + this.threadId);
            //console.log('Depth    : ' + this.depth);
            //console.log('Errornr  : ' + this.err);
            console.log('===============================================================================');
        }
    });
    // Print out data array, which will contain de/encrypted data as output
    function dumpAddr(info, addr, size) {
        if (addr.isNull())
            return;
        console.log('Data dump ' + info + ' :');
        var buf = Memory.readByteArray(addr, size);
        // If you want color magic, set ansi to true
        console.log(hexdump(buf, { offset: 0, length: size, header: true, ansi: false }));
    }
    function resolveAddress(addr) {
        // Enter the base address of dll as seen in your favorite disassembler (here IDA)
        var idaBase = ptr('0x77EC0000');
        // Calculate offset in memory from base address in IDA database 
        var offset = ptr(addr).sub(idaBase);
        // Add current memory base address to offset of function to monitor 
        var result = baseAddr.add(offset); 
        // Write location of function in memory to console
        console.log('[+] New addr=' + result); 
        return result;
    }
""")
 
    script.on('message', on_message)
    script.load()
    print("[!] Ctrl+D on UNIX, Ctrl+Z on Windows/cmd.exe to detach from instrumented program.\n\n")
    sys.stdin.read()
    session.detach()
    
if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("this script needs pid or proc name :(")
        sys.exit(1)
 
    try:
        target_process = int(sys.argv[1])
    except ValueError:
        target_process = sys.argv[1]
    main(target_process)
cs

Frida공식 홈페이지를 방문하면 기본적인 틀에 대한 설명과 예제 코드를 볼 수 있습니다. 저 역시 아래 링크의 폼에서 수정을 하며 완성을 시켰습니다.

[링크] https://www.frida.re/docs/examples/windows/


2-1 후킹 메서드 주소 찾기

먼저, 해당 __vbaStrCmp 메서드 주소를 찾는 방법입니다.

var baseAddr = Module.findBaseAddress('MSVBVM50.dll');

const __vbaStrCmp = Module.findExportByName("MSVBVM50.dll", "__vbaStrCmp");


위 코드를 실행할 때는 python script_name.py [PID] 로 실행하게 됩니다. 프로세스를 타겟팅해서 attach 하는 것이지요

타겟 프로세스에서 로드되어있는 dll의 주소를 찾기 위해서는 findBaseAddress()를 사용하여 찾을 수 있습니다.


그리고 우리가 후킹을 걸기 위해 필요한 함수의 주소는 findExportByName("dll명", "함수명") 으로 찾을 수 있죠

따라서 const __vbaStrCmp = Module.findExportByName("MSVBVM50.dll", "__vbaStrCmp"); 이 코드가 실행되고 나면 __vbaStrCmp의 주소가

변수에 담기게 됩니다.


프로세스 내부에 존재하는 사용자 정의함수 같은 경우에는 var print_log = resolveAddress('0x0043FC34'); 와 같은 형식으로 얻어와야합니다.

resolveAddress()에 IDA에서 확인한 주소 값(sub_xxxxxxxx)을 인자로 전달해주고, resolveAddress함수 안에 idaBase변수에 IDA에서 사용한 베이스 주소를 입력해준 후 사용해야 합니다.


2-2 onEnter

타겟 함수에 제대로 attach가 되었다면 이제 값을 자유롭게 보고, (권한이 허용되는 한)조작할 수 있습니다.

1
2
3
4
5
6
7
8
9
onEnter: function (args) {
            console.log('===============================================================================');
            console.log('[+] Called __vbaStrCmp!! [' + __vbaStrCmp + ']');
            console.log('[+] args[0] = [' + args[0+ ']');
            dumpAddr('args[0]', args[0], 0x16); 
 
            console.log('[+] args[1] = [' + args[1+ ']');
            dumpAddr('args[1]', args[1], 0x16);
}
cs

onEnter에서는 args변수로 함수 인자 값들을 확인해 볼 수 있습니다. 지금은 두 인자 값이 포인터로 전달이 되기 때문에 hexdump를 떠서 

확인해보고있습니다.


2-3 onLeave

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
onLeave: function (retval) {
    console.log('===============================================================================');
            
    /*
    It doesn't work !
    console.log('[+] (Origin) Returned from __vbaStrCmp: ' + typeof(retval));
    console.log('[+] (Origin) Returned from __vbaStrCmp: ' + retval);
    retval = 0;
    console.log('[+] (forgery) Returned from __vbaStrCmp: ' + typeof(retval));
    console.log('[+] (forgery) Returned from __vbaStrCmp: ' + retval);
    */
    this.context.eax = 0x0;
    console.log('Context information:');
    console.log('Context  : ' + JSON.stringify(this.context));
    //console.log('Return   : ' + this.returnAddress);
    //console.log('ThreadId : ' + this.threadId);
    //console.log('Depth    : ' + this.depth);
    //console.log('Errornr  : ' + this.err);
    console.log('===============================================================================');
}
cs

__vbaStrCmp는 문자열 비교 결과 일치하는 경우 0을 리턴하고 다른경우 -1을 리턴하는 것으로 보입니다.

따라서 1차 적으로는 retval 을 0 으로 만들어 주었었는데요, 확인을 해보니 retval이 바뀌었지만 제대로 동작하지 않는 모습을 보였습니다.

왜인지는 모르겠어요ㅠㅠ


이후에 함수의 리턴값을 저장하는 eax의 값을 확인해보니 0xffffffff로 -1이 저장되고 있는 것을 확인하였고, 

this.context.eax = 0x0; 로 eax값을 0으로 만들어 준 후 정상적으로 크래킹된 것을 볼 수 있었습니다.



끗!

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

Windows Hooking(Frida)  (4) 2018.01.30
  1. Juice 2018.10.05 14:09

    와 프리다 분석에 정말 많은 도움됐습니다. 감사합니다. 10000따봉 박고갑니다!

    • JSBach Bach 2018.10.27 00:02 신고

      감사합니다~
      최근에 알게된 것을 덧붙혀드리자면
      args는 stack에 있는 값을 가져오는 것같습니다. 콜 방식에 따라 인자 값을 레지스터에서 가져와야 할 수도있단 말이죠,

      return 값도 eax에 있는 값을 참조해서 뿌려주는 것 같습니다.

  2. Juice 2018.10.30 10:26

    저의경우 onLeave에서 retval=0; 을 하면 정상적으로 반환값이 변조되는데 context로 eai 변조할 수 있는건 처음 알았네요!
    다시 보다가 또 따봉드리고갑니다!

  3. Juice 2018.10.30 10:30

    아 저는 retval.replace(0); 으로 하였습니다!..

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)  (2) 2017.05.06
iOS Hooking#2(Frida)  (0) 2017.04.19
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

+ Recent posts