[Frida] FridaLab 문제 풀이

6 분 소요

💡 FridaLab 문제에 대한 풀이입니다.

개요

본 포스팅에서는 Frida Lab 문제들에 대한 풀이를 해보겠습니다. FridaLab을 풀기위한 환경설정들은 이전 포스팅을 참고해 주시면 되겠습니다.


함수 사용법

문제를 풀때, 아래의 함수들을 사용할 것입니다. 자세한 설명은 Frida Docs를 참고해 주시면 되겠습니다.

📗 Javascript API Documentation
  • Java.perform(function()) : 현 재 실행중인 스레드에서 Java 코드를 실행하겠다고 선언하는 것입니다. 실행할 코드는 function(){``선언``} 부분에 작성해 주면 됩니다.
  • Java.use(classname) : classname에 주어진 이름의 클래스를 해당 프로세스에서 찾아줍니다. class를 찾으면 해당 클래스의 주소를 리턴해주며, 이를 이용해서 객체를 생성하거나, static 함수를 호출 할 수 있습니다.
  • Java.choose(classname,callbacks) : java.use는 객체가 아닌 클래스를 찾는다면, Java.choose는 프로세스에서 실행중인 객체를 찾는 것입니다. callbacks 함수는 다음 2가지 함수를 작성해 줘야 합니다.
    • onMatch(instance) : 클래스 이름에 해당하는 객체를 찾을 때 마다 호출되는 함수입니다. instance는 해당 객체를 가리키는 변수입니다.
    • onComplete() : 모든 인스턴스들의 탐색을 끝냈을 때 호출해 주는 함수입니다.

이 외에도 많은 함수들이 있는데 Fridalab 문제들을 풀 때는 위의 함수들만 잘 이해해도 풀 수 있습니다.


파이썬 코드 틀

문제를 풀때, 파이썬을 이용해서 문제를 풀 수 있습니다. 모든 문제를 풀 때, 파이썬을 작성하는 틀은 동일하며 Javascript 코드 부분만 변경해서 문제를 풀게 됩니다. 문제를 풀 때 사용하는 파이썬 틀은 다음과 같습니다.

import frida,sys

def on_message(message,data):
    print(message)

# 실행할 코드를 javascript 문법으로 작성해 줍니다.
jscode="""
Java.perform(function(){
  ...
  함수 선언
  ...
})
"""

# frida-server가 설치된 usb_device에 연결을 하고, 'FridaLab' 프로세스와 세션을 만든다.
session = frida.get_usb_device().attach("FridaLab")

# 세션에 전송할 스크립트 코드를 담는다.
script = session.create_script(jscode)

# 해당 세션에서 시그널을 비동기적으로 처리하는 함수로, 디버깅을 위한 용도. 세션에서 message 시그널이 오면 출력 해줍니다.
script.on("message",on_message)

# 실질적으로 코드가 실행되는 부분으로 성공적으로 되었다면 응답이 없고, 오류가 난다면 messsage시그널을 보내줍니다.
script.load()

# frida 프로세스가 끝나면, 변조한 값들이 다시 돌아가기 때문에 ctr+c로 끝내기 전까지는 프로세스를 끝내지 않도록 처리합니다.
sys.stdin.read()


Jadx

우선 문제들을 풀기 위해서는 Jadx를 통해서 디컴파일된 코드를 분석해야 합니다. 다음과 같이 jadx-gui를 통해서 클래스가 어떻게 정의되었는지 확인을 하면서 문제풀이를 진행하면 됩니다.

image


Challenge 1

첫번째 문제입니다.

Change class challenge_01's variable `chall01` to 1

challenge_01 클래스의 변수 chall01을 1로 바꾸는 문제입니다. jadx로 디컴파일된 코드를 보면 challenge_01 클래스는 다음과 같이 선언되어 있습니다.

package uk.rossmarks.fridalab;

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

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

변수 chall01이 static으로 선언되어 있기 때문에, 해당 클래스를 찾아서 static 변수를 다음처럼 바꿔주면 됩니다.

Java.perform(function(){
  var chall01Class = Java.use('uk.rossmarks.fridalab.challenge_01');
  var chall01Class.chall01.value = 1;
})


Challenge 2

Run chall02()
public class MainActivity extends AppCompatActivity {
    // .. 중략

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

chall02() 함수는 MainActivity 클래스 내에 있는 instance Method 입니다. 따라서 chall02()함수를 호출하기 위해서는 MainActivity 클래스의 Instance를 찾고, 해당 Instance 내에 있는 chall02()함수를 호출해야 합니다.

이번문제는 chall02()함수가 static method가 아닌, Instance Method 이기 때문에 Java.use() 함수가 아닌, Java.choose()함수를 통해서 현재 실행중인 Java 객체를 찾아와야 합니다.

참고로 Java.use()함수를 이용해서 다음처럼 chall02() 함수를 호출하려고 한다면 오류 메시지가 출력됩니다.

// 오답
Java.perform(function(){
  var MainActivityClass = Java.use('uk.rossmarks.fridalab.MainActivity');
  MainActivityClass.chall02();
})

image

따라서 다음과 같이 Java.choose() 함수를 이용해서 실행중인 Instance를 찾은다음, 해당 instance를 main에 넣음으로써, 객체를 사용할 수 있습니다.

Java.perform(function(){
  var main;
  Java.choose('uk.rossmarks.fridalab.MainActivity',{
    onMatch: function(instance){
      main = instance
    },
    onComplete: function(){}
  })

  // main 에는 main instance가 들어가 있음
  main.chall02();
})


Challenge 3

Make chall03() return true

chall03()함수는 MainActivity 클래스 내의 인스턴스 함수로 선언되어 있습니다.

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

    public boolean chall03() {
        return false;
    }
}

현재 return false로 선언되어 있기 때문에 main instance에서 chall03()함수를 overload하여 return true로 바꿔줄 수 있습니다.

Java.perform(function(){
  var main;
  Java.choose('uk.rossmarks.fridalab.MainActivity',{
    onMatch: function(instance){
      main = instance
    },
    onComplete: function(){}
  })

  // main 에는 main instance가 들어가 있음
  main.chall03.overload().implementation = function(){
    return true;
  }
})

이때 overload 문법은 overload()로 하면 인자를 받지 않는 chall03()함수를 overload한다는 뜻이고, 만약 chall03(int) 처럼 int형 인자를 받아서 처리하는 함수라면, main.chall03.overload(int) 와 같이 overload해줘야 합니다.


Challenge 4

send "frida" to chall04()

2,3번과 동일하게 main instance에서 chall04(“frida”)를 호출해 주면 됩니다.

Java.perform(function(){
  var main;
  Java.choose('uk.rossmarks.fridalab.MainActivity',{
    onMatch: function(instance){
      main = instance
    },
    onComplete: function(){}
  })

  // main 에는 main instance가 들어가 있음
  main.chall04("frida");
})


Challenge 5

Always send "frida" to chall05()

chall05() 메서드는 MainActivity 안에 존재하는 Instance method 입니다. 3번과 동일하게 해당 함수를 overload 해줍니다.

public void chall05(String str) {
        if (str.equals("frida")) {
            this.completeArr[4] = 1;
        } else {
            this.completeArr[4] = 0;
        }
    }
Java.perform(function(){
  var main;
  Java.choose('uk.rossmarks.fridalab.MainActivity',{
    onMatch: function(instance){
      main = instance
    },
    onComplete: function(){}
  })

  // main 에는 main instance가 들어가 있음
  main.chall05.ovrlaod('java.lang.String').implementation = function(){
    this.chall05("frida");
    console.log("chall05 method is overloaded!");
})


Challenge 6

Run chall06() after 10 seconds with correct value

코드 분석

// MainActivity 생성자에서 스케쥴러를 만들어서 challenge_06.addchall06() 함수를 매 초마다 실행
public class MainActivity extends AppCompatActivity {
    
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        // .. 생략
        challenge_06.startTime();
        challenge_06.addChall06(new Random().nextInt(50) + 1);
        new Timer().scheduleAtFixedRate(new TimerTask() { 
            public void run() {
                int nextInt = new Random().nextInt(50) + 1;
                challenge_06.addChall06(nextInt);
                Integer.toString(nextInt);
            }
        }, 0L, 1000L);
    }
    // .. 생략

    // confirmChall06(i) 함수가 true를 반환하면 된다.
    public void chall06(int i) {
      if(challenge_06.confirmChall06(i)) {
          this.completeArr[5] = 1;
        }
    }

}




// challenge_06 클래스 구조
public class challenge_06 {
    static int chall06;
    static long timeStart;

    //startTime()이 호출되면 시스템의 현재 시간을 저장
    public static void startTime() {
        timeStart = System.currentTimeMillis();
    }
    

    public static boolean confirmChall06(int i) {
        return i == chall06 && System.currentTimeMillis() > timeStart + 10000;
    }
    
    // 스케줄러에서 넘겨받은 정수를 chall06변수에 더함
    public static void addChall06(int i) {
        chall06 += i;
        if (chall06 > 9000) {
            chall06 = i;
        }
    }
}

코드를 보면, MainActivity 생성자에서 스케줄러를 생성하여, challenge_06.addChall06(nextInt)를 매 초마다 실행하고 있습니다. 따라서 challenge_06.chall06 의 값은 매 초마다 랜덤하게 바뀌고 있습니다.

클리어 조건인 challenge_06.confirmChall06(int i) 함수는 2가지 조건이 있습니다.

  1. i == chall06
  2. system.currentTimeMills() > timeStart + 10000;

confirmChall06을 호출할때, chall06의 값을 넘겨주고, 앱이 실행된지 10초이상 경과했으면 풀리는 문제입니다. 10초이상 경과후에 chall06 함수를 실행하기 위해 setTimeout()함수를 이용해 줍니다.

Java.perform(function(){
  var main;
  Java.choose('uk.rossmarks.fridalab.MainActivity',{
    onMatch: function(instance){
      main = instance;
    },
    onComplete: function(){}
  })

  setTimeout(function(){
        var chall06Class = Java.use("uk.rossmarks.fridalab.challenge_06");
        main.chall06(chall06Class.chall06.value);
        console.log("[+] challenge 06 solved!");
    },10000)
})


Challenge 7

Bruteforce check07Pin() then confirm with chall07()

MainActivity에서 chall07을 세팅 및 호출하는 부분과, challenge_07 class의 정의 부분은 아래와 같습니다.

public class MainActivity extends AppCompatActivity {
    
    public void onCreate(Bundle bundle) {
        // .. 생략
        // 생성자에서 setChall07() 함수 한번 호출
        challenge_07.setChall07();
    };

    // .. 생략
    // challenge_07.check07Pin(str) 함수가 true면 성공
    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);
    }
}

challenge_07 의 check07Pin(str)을 호출했을 때 True가 반환되면 해당 숫자를 check07(str)로 넘겨줘서 클리어 할 수 있습니다.

Java.perform(function(){
  var main;
  Java.choose('uk.rossmarks.fridalab.MainActivity',{
    onMatch: function(instance){
      main = instance;
    },
    onComplete: function(){}
  })

  var chall07Class = Java.use("uk.rossmarks.fridalab.challenge_07");
  var i;
  // check07Pin()의 결과가 true인 i를 찾아서 chall07(i)호출.
  for(i=1000;i<9999;i++){
      if(chall07Class.check07Pin(i.toString())){
          main.chall07(i.toString())
          console.log("[+] challenge 07 solved!");
      }
    }
})


Challenge 8

Change 'check' button's text value to 'Confirm'

화면에 나오는 ‘CHECK’ 버튼의 텍스트를 ‘Confirm’으로 바꿔야 합니다.

우선 해당 ‘check’ 버튼의 View를 찾아야 합니다. MainActivity의 생성자를 보면 다음과 같이 R.id.check에서 view를 가져와서 Button으로 캐스팅 시켜서 생성한 것을 볼 수 있습니다. 따라서 동일한 방식으로 MainActivity instance에서 findViewById로 view의 주소를 찾은다음, Button으로 캐스팅 시킨다음 Button.setText()함수를 통해서 Text를 덮어씌울 수 있습니다.

public class MainActivity extends AppCompatActivity {

    public void onCreate(Bundle bundle) {
        setContentView(R.layout.activity_main);
        ((Button) findViewById(R.id.check)).setOnClickListener(new View.OnClickListener() { /
            @Override // android.view.View.OnClickListener
            public void onClick(View view) {

            }
        });

위의 내용을 코드로 구현하면 다음과 같습니다.

    var RIdClass = Java.use('uk.rossmarks.fridalab.R$id');
    var buttonClass = Java.use('android.widget.Button');
    var StringClass = Java.use('java.lang.String');
    // R.id.check 값을 구해옵니다.
    var RIdCheck = RIdClass.check.value; 
    // MainActivity의 findViewById를 이용해서 view를 찾습니다.
    var view = main.findViewById(RIdCheck);
    // view를 buttonClass로 캐스팅 하여 Button 객체로 만들어 줍니다.
    var button = Java.cast(view,buttonClass);
    // Button.setText함수를 이용하여 글자를 수정해 줍니다.
    button.setText(StringClass.$new("Confirm"));
    console.log("[+] challenge 08 solved!");

이렇게 해서 총 8문제를 모두 클리어 할 수 있습니다.

image

전체 소스코드

전체 소스코드는 다음과 같습니다.

깃허브 : https://github.com/kangmyoungseok/Frida

댓글남기기