본문 바로가기

보안 프로젝트

모바일 해킹을 위한 JADX & Frida with FridaLab

반응형

JADX & Frida를 이용한 모바일 어플리케이션 학습을 위한 예제로 FridaLab을 이용해 보겠습니다.

https://rossmarks.uk/blog/fridalab/

 

FridaLab

I was struggling with a recent test using frida, knowing it could do what I want but unsure how. After lots of googling and trial and error I eventually got it working. So I decided

rossmarks.uk

 

frida 설치의 경우

pipx install frida-tools

mac 의 terminal에서 해당 명령어로 설치하고

 

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

 

Releases · frida/frida

Clone this repo to build Frida. Contribute to frida/frida development by creating an account on GitHub.

github.com

 

이곳에서 맥북을 사용하는 저는 frida-server-17.3.2-android-arm64.xz 를 설치하였습니다.

 

마지막으로 android studio에서 원하는 Emulator를 설치해서 이용하였습니다. (Pixel 7)

 

또한 Frida를 사용하려면 root 권한은 필수이므로 Emulator를 루팅해줍니다.

adb shell
su
cd /data/local/tmp
chmod 755 frida-server
./frida-server &

 

세팅이 마무리 되었다면 frida와 JADX를 통해 모바일 어플리케이션 모의해킹을 진행해봅시다.

 

1. Emulator 실행

- Android Studio -> More Actions -> Virtual Device Manager

 

2. JADX

- finder로 /jadx/build/jadx/bin으로 이동 후 jadx-gui 실행

 

3. Frida

adb shell
su
/data/local/tmp/frida-server &
exit
exit

 

 

이제 정말 세팅이 마무리 되었습니다.

이제부터 FridaLab에 대한 모의 해킹을 진행해보겠습니다.

 

FridaLab Challenge

1.  개요

1) JADX를 이용한 취약점 분석

JADX는 안드로이드 APK파일을 디컴파일하여 JAVA코드로 변환해주는 도구입니다.

이를 이용해 주로 maifest 파일에서의 권한 정의를 분석하고, 각 파일들의 소스코드를 보며 하드코딩된 민감정보나 암호화 알고리즘 분석

그리고 보안 로직 우회점읠 찾을 수 있습니다.

 

2) Frida를 이용한 hooking 코드 작성 방식

Frida hooking 코드는 주로 Javascript로 작성됩니다.

그리고 해당 코드는 크게 두가지 방식으로 작동됩니다.

 첫번 쨰로는 Function 방식입니다.

Java.perform(function () {
  ...
});

 

해당 방식은 코드가 길어지거나 로직이 복잡할 때 가독성이 좋고, 재사용이 용이합니다.

 

두번 쨰로는 Lambda 방식입니다.

Java.perform(() => {
  ...
});

 

해당 방식은 코드가 짧고 직관적이어서 간단한 로직을 작성할 때 가독성을 높일 수 있습니다.

 

3) FridaLab

 

Emulator에서 FridaLab을 처음 실행하면 풀어야 할 문제 8개를 제시하고 있습니다.

이제부터 해당 문제를 하나씩 풀어보겠습니다.

2. Challenge 1

우선 해야 할 일은 Change class challenge_01's variable 'chall01' to:1 입니다.

public class MainActivity extends AppCompatActivity {
    public int[] completeArr = {0, 0, 0, 0, 0, 0, 0, 0};

    public boolean chall03() {
        return false;
    }

    @Override // android.support.v7.app.AppCompatActivity, android.support.v4.app.FragmentActivity, android.support.v4.app.SupportActivity, android.app.Activity
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.activity_main);
        ((Button) findViewById(R.id.check)).setOnClickListener(new View.OnClickListener() { // from class: uk.rossmarks.fridalab.MainActivity.1
            @Override // android.view.View.OnClickListener
            public void onClick(View view) {
                if (challenge_01.getChall01Int() == 1) {
                    MainActivity.this.completeArr[0] = 1;
                }

 

mainActivity를 보시면 이와 같이 challenge_01의 getChall01Int()의 return값이 1이 되면 정답처리 되는 것으로 보입니다.

package uk.rossmarks.fridalab;

/* loaded from: classes.dex */
public class challenge_01 {
    static int chall01;

    public static int getChall01Int() {
        return chall01;
    }
}

 

따라서 위 코드에서 chall01이라는 static 변수의 값을 1로 바꿔주는 hooking code를 작성해주시면 됩니다.

 

Java.perform(function(){
    console.log("frida hooking start");

    //후킹할 클래스 선언
    var c_chall01 = Java.use("uk.rossmarks.fridalab.challenge_01");

    c_chall01.chall01.value = 1;
    console.log("frida hooking end");
});

 

hooking code는 위와 같이 작성하였습니다.이를 frida를 이용해 apk에 삽입하면

 

위와 같이 삽입할 때는 새로운 terminal window를 띄워서 

frida -U -f [target APK package name] -l [hooking code file]

 

그 결과 아래 이미지와 같이 초록색으로 1번 문제가 바뀌며 해결이 됨을 알 수 있습니다.

 

3. Challenge 2

이번에 해야 할 일은 Run chall02() 입니다.

* 이제부터 각 문제 이미지는 생략하겠습니다. 

private void chall02() {
	this.completeArr[1] = 1;
}

 

해당 method를 hooking code를 통해 강제로 실행시켜야 하며,

이는 MainActivity에 선언되어 있습니다.

 

우선 hooking code는 아래와 같습니다.

Java.perform(function(){
    var MainActivity = Java.use("uk.rossmarks.fridalab.MainActivity");

    MainActivity.onResume.implementation = function(){
        this.onResume();
        MainActivity.chall02.call(this);
    }
});

 

MainActivity.onResume.implimantation = function() {} 을 통해 

MainActivity의 onResume() method의 원래 implementation을 제가 새로 만든 function으로 덮어 씌웁니다.

 

onResume()을 덮어 씌운 이유로는 onResume() method가 android Activity의 Lifecycle에서 화면이 사용자에게 완전히 보여지고 상호작용이 가능해지는 시점에서 호출되도록 약속된 method입니다.

이를 통해 MainActivity 객체가 이미 성공적으로 생성되어 메모리에 존재할 때 활용함을 보장받을 수 있습니다.

 

그리고 이러한 function안에서 this.onResume()을 통해 원래의 onResume이 하던 일을 그대로 실행해줌으로써 앱의 정상적인 흐름을 보장하고 충돌을 방지합니다.

 

이후에는 chall02 method가 private이므로 .call(this)를 통해 onResume()이 실행된 instance를 context로 지정하여 강제로 실행시킵니다.

 

4. Challenge 3

이번에 해야 할 일은 Make chall03() return true 입니다.

chall03() method의 경우 MainActivity에 아래와 같이 정의되어 있습니다.

public boolean chall03() {
        return false;
    }

 

해당 method를 hooking code를 통해 return값을 false에서 true로 바꿔주면 됩니다.

hooking code는 아래와 같습니다.

Java.perform(function(){
    console.log("frida hooking start");
    
    var MainActivity = Java.use("uk.rossmarks.fridalab.MainActivity");

    MainActivity.chall03.implementation = function(){
        return true;
    }
});

 

MainActivity.chall03.implimantation = function() {} 을 통해 

MainActivity class에 정의된 chall03 method의 원래 로직을 제가 새로 만든 function에 덮어 씌웁니다.

그렇게 하면 3번 문제는 clear됩니다.

 

* 추가적으로 다뤄야 할 내용으로 가끔 frida-server가 오류가 날 때가 있습니다.

이에 대하여 

adb shell
su
killall -9 frida-server
exit
exit

이를 통해 frida-server process를 종료시키고 다시 실행하면 해결 될 수 있습니다.

 

5. Challenge 4

이번에 해야 할 일은 Send "frida" to chall04() 입니다.

chall04() method의 경우 MainActivity에 아래와 같이 정의되어 있습니다.

   public void chall04(String str) {
        if (str.equals("frida")) {
            this.completeArr[3] = 1;
        }
    }

 

해당 method에 parameter str에 "frida"를 넣어서 호출하면 해결됩니다.

이에 대한 hooking code로는 아래와 같습니다.

Java.perform(function(){
    var MainActivity = Java.use("uk.rossmarks.fridalab.MainActivity");

    MainActivity.onResume.implementation = function(){
        this.onResume();
        MainActivity.chall04.call(this, "frida");
    }
});

 

이렇게 하면 4번 문제는 해결됩니다.

6. Challenge 5

이번에 해야 할 일은 Always send "frida" to chall05() 입니다.

chall05() method의 경우 MainActivity에 아래와 같이 정의되어 있습니다.

    public void chall05(String str) {
        if (str.equals("frida")) {
            this.completeArr[4] = 1;
        } else {
            this.completeArr[4] = 0;
        }
    }

 

4번과 같은 문제처럼 보이지만 아래와 같이 MainActivity에서 chall05() method를 호출할 때 

MainActivity.this.chall05("notfrida!");

 

이와 같이 정답 버튼(check)를 누르면 parameter로 "notfrida"를 넣어서 호출되고 있습니다.

 

따라서 해당 문제의 핵심은 chall05() method가 호출되는 순간을 hooking해서

해당 notfrida param을 frida로 바꿔주어야 합니다.

 

따라서 이에 대한 hooking code는 아래와 같습니다.

Java.perform(function(){
    var MainActivity = Java.use("uk.rossmarks.fridalab.MainActivity");

    MainActivity.chall05.overload('java.lang.String').implementation = function(str){
        this.chall05("frida");
    }
});

 

chall05.overload('java.lang.String')을 이용해 overloading되어 있을 수도 있는 method에 대하여

String을 parameter로 하는 chall05() method를 특정해 줍니다.

 

또한 .implementation = function(str){} 를 통해 chall05() method를 제가 만든 코드로 덮어씌우며

기존에 받아왔던 str 변수를 function(str)로 받아줌을 지정합니다.

 

또한 해당 hooking code는 onResume과 같은 lifecycle method로 chall05()기준으로 외부 함수가아닌

자기 자신을 호출하므로 그냥 this.chall05 이렇게 호출이 가능합니다. 

 

이렇게 하면 5번 문제는 해결됩니다.

7. Challenge 6

이번에 해야 할 일은 Run chall06() after 10 seconds with correct value 입니다.

chall06() 과 관련된 method의 경우 MainActivity에 아래와 같이 정의되어 있습니다.

@Override // android.view.View.OnClickListener
            public void onClick(View view) {
                if (challenge_01.getChall01Int() == 1) {
                    MainActivity.this.completeArr[0] = 1;
                }
                if (MainActivity.this.chall03()) {
                    MainActivity.this.completeArr[2] = 1;
                }
                MainActivity.this.chall05("notfrida!");
                if (MainActivity.this.chall08()) {
                    MainActivity.this.completeArr[7] = 1;
                }
                MainActivity.this.changeColors();
            }
        });
        challenge_06.startTime();
        challenge_06.addChall06(new Random().nextInt(50) + 1);
        new Timer().scheduleAtFixedRate(new TimerTask() { // from class: uk.rossmarks.fridalab.MainActivity.2
            @Override // java.util.TimerTask, java.lang.Runnable
            public void run() {
                int iNextInt = new Random().nextInt(50) + 1;
                challenge_06.addChall06(iNextInt);
                Integer.toString(iNextInt);
            }
        }, 0L, 1000L);
        
           public void chall06(int i) {
        if (challenge_06.confirmChall06(i)) {
            this.completeArr[5] = 1;
        }
    }

 

또한 이에 대한 challenge_06 class의 경우 아래와 같이 따로 정의되어 있습니다.

package uk.rossmarks.fridalab;

/* loaded from: classes.dex */
public class challenge_06 {
    static int chall06;
    static long timeStart;

    public static void startTime() {
        timeStart = System.currentTimeMillis();
    }

    public static boolean confirmChall06(int i) {
        return i == chall06 && System.currentTimeMillis() > timeStart + 10000;
    }

    public static void addChall06(int i) {
        chall06 += i;
        if (chall06 > 9000) {
            chall06 = i;
        }
    }
}

 

해당 문제를 해결하기 위해서는 challenge_06 class에 정의 되어 있는 confirmChall06(i)가 true여야만 문제를 맞출 수 있습니다.

이에 대한 조건으로는 10초 후의 confirmchall06의 param으로 들어갈 난수 i가 chall06 변수가 같아야 합니다.

 

아래는 이에 대한 후킹 코드입니다.

Java.perform(function(){
    const MainActivity = Java.use("uk.rossmarks.fridalab.MainActivity");

    MainActivity.onResume.implementation = function(){
        this.onResume();

        const c_chall06 = Java.use("uk.rossmarks.fridalab.challenge_06");

        c_chall06.addChall06.implementation = function(i){
            console.log("addChall06 method 무력화");
        }

        c_chall06.confirmChall06.implementation = function(i){
            return true;
        }

        MainActivity.chall06.call(this, 1);
    }
})

해당 문제는 addChall06 method에 공백을 덮어써 무력화 시켜주고, confirmChall06() method에서 return 을 true로 반드시 반환되게 hooking 해주면 해당 문제는 해결됩니다.

8. Challenge 7

이번에 해야 할 일은 Bruteforce check07Pin() then confirm with chall07() 입니다.

chall07() 과 관련된 method의 경우 MainActivity에 아래와 같이 정의되어 있습니다.

challenge_07.setChall07();

public void chall07(String str) {
	if (challenge_07.check07Pin(str)) {
    	this.completeArr[6] = 1;
    } else {
    	this.completeArr[6] = 0;
    }
}

 

또한 challenge_07 class는 다른 파일에 따로 정의되어 있습니다.

public class challenge_07 {
    static String chall07;

    public static void setChall07() {
        chall07 = BuildConfig.FLAVOR + (((int) (Math.random() * 9000.0d)) + 1000);
    }

    public static boolean check07Pin(String str) {
        return str.equals(chall07);
    }
}

 

이에 대한 후킹 코드로는 간단합니다.

Java.perform(function(){
    const MainActivity = Java.use("uk.rossmarks.fridalab.MainActivity");
    const c_chall07 = Java.use("uk.rossmarks.fridalab.challenge_07");

    MainActivity.onResume.implementation = function(){
        this.onResume();

        for(let i=1000; i<10000; i++){
            let pin = i.toString();

            if(c_chall07.check07Pin(pin)){
                MainActivity.chall07.call(this, pin);
                break;
            }
        }
    }
});

 

이렇게 하면 문제가 풀리게 됩니다.

9. Challenge 8

이번에 해야 할 일은 Change 'check' button's text value to 'Confirm' 입니다.

chall08() 과 관련된 method의 경우 MainActivity에 아래와 같이 정의되어 있습니다.

    public boolean chall08() {
        return ((String) ((Button) findViewById(R.id.check)).getText()).equals("Confirm");
    }

 

해당 문제를 해결하기 위해서는 check button의 text를 "Confirm"으로 바꿔야 합니다.

이에 대한 hooking code는 아래와 같습니다.

Java.perform(function(){
    const MainActivity = Java.use("uk.rossmarks.fridalab.MainActivity");

    MainActivity.onResume.implementation = function(){
        this.onResume();

        const resources = this.getResources();
        const packageName = this.getPackageName();
        const check_id = resources.getIdentifier("check", "id", packageName);

        const check_btn = this.findViewById(check_id);

        const check_button = Java.cast(check_btn, Java.use("android.widget.Button"));
        const JavaString = Java.use('java.lang.String');

        const set_string = JavaString.$new("Confirm");

        check_button.setText(set_string);



    }
});

 

R.id.check라는 button을 추적하기 위해 getResource 객체를 불러줍니다. 

또한 getPackageName을 통해 리소스를 찾을 때 주소를 명확히 하기 위해 앱의 고유 이름을 가져옵니다.

 

이들을 이용하여 getIdentifier를 통해 해당 package에 있는 id.check resource의 ID를 알아냅니다.

이렇게 알아낸 ID를 이용하여 우리가 바꿔야 되는 button을 찾아냅니다.

허나, 해당 버튼을 찾아도 일반적으로 view type으로 찾아주나, (다형성 이슈로 인해 부모 class = View, 자식 class = Button)

Java.case를 통해 button type으로 타입을 변환시켜 setText method를 사용할 수 있게 해줍니다.

 

이후에는 Java코드를 javascript로 hooking하다보니 생기는 일종의 통역과정에서의 문제를 해결하기 위해 Java.use('java.lang.String')을 통해 Confirm이라는 내용물을 가진 Java String 객체를 만들어주고

setText를 이용해 문제의 요구조건을 충족하면 문제가 풀리게 됩니다.

 

이렇게 Fridalab을 통해 Frida 기초를 배워보았습니다.

감사합니다.

 

 

 

반응형