본문 바로가기
안드로이드 해킹/Frida-Labs

[Frida-Labs] Frida 사용 실습 with Frida Labs 문제 풀이 1 ~ 7

by jwcs 2025. 1. 13.
728x90
반응형

문제 풀이를 진행할 frida lab이 다른 블로그에서 보이는 frida lab과는 달라 보인다. 하지만 공부하기에 좋았기 때문에 기록하기로 했다. 문제 풀이를 하면서 문법 및 사용 방법에 대해 익혀보자.

 

[설치]

https://github.com/DERE-ad2001/Frida-Labs

 

GitHub - DERE-ad2001/Frida-Labs: The repo contains a series of challenges for learning Frida for Android Exploitation.

The repo contains a series of challenges for learning Frida for Android Exploitation. - DERE-ad2001/Frida-Labs

github.com

여기서 다운받으면 된다.

 

[Frida 사용 기초]

frida-ps -Uai

만약 설치된 패키지를 검색해야한다면 위와 같은 명령어를 사용할 수 있다.

frida-ps -Uai

  • frida-ps
    • 현재 디바이스에서 실행 중인 프로세스 목록을 표시하는 명령어
  • -U
    • USB로 연결된 디바이스(혹은 NOX와 같은 에뮬레이터)로 제한하여 검색하는 옵션
  • -a
    • 모든 프로세스(시스템 포함)를 표시
  • -i
    • 앱의 정보 (executable path) 출력
frida -U -f <package_name>

프리다를 애플리케이션에 연결하기 위해선 위와 같이 명령어를 사용하면 된다.

  • -f
    • 프로세스를 강제로 실행(fork)하면서 후킹을 시작하는 옵션
frida -U -f com.ad2001.calculator

문제 풀이를 위해 사용한 실제 명령어는 위와 같다.

 

[0x1 문제]

초기 페이지

위 화면이 메인 화면이다. 입력창에 올바른 값이 아닐 경우 Try again이 출력된다. 코드를 살펴보자.

 

public class MainActivity extends AppCompatActivity {

    /* renamed from: t1 */
    TextView f103t1;

    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(C0498R.layout.activity_main);
        final EditText editText = (EditText) findViewById(C0498R.id.editTextTextPassword);
        Button button = (Button) findViewById(C0498R.id.button);
        this.f103t1 = (TextView) findViewById(C0498R.id.textview1);
        final int i = get_random();
        button.setOnClickListener(new View.OnClickListener() { // from class: com.ad2001.frida0x1.MainActivity.1
            @Override // android.view.View.OnClickListener
            public void onClick(View view) {
                String obj = editText.getText().toString();
                if (TextUtils.isDigitsOnly(obj)) {
                    MainActivity.this.check(i, Integer.parseInt(obj));
                } else {
                    Toast.makeText(MainActivity.this.getApplicationContext(), "Enter a valid number !!", 1).show();
                }
            }
        });
    }

    int get_random() {
        return new Random().nextInt(100);
    }

    void check(int i, int i2) {
        if ((i * 2) + 4 == i2) {
            Toast.makeText(getApplicationContext(), "Yey you guessed it right", 1).show();
            StringBuilder sb = new StringBuilder();
            for (int i3 = 0; i3 < 20; i3++) {
                char charAt = "AMDYV{WVWT_CJJF_0s1}".charAt(i3);
                if (charAt < 'a' || charAt > 'z') {
                    if (charAt >= 'A') {
                        if (charAt <= 'Z') {
                            charAt = (char) (charAt - 21);
                            if (charAt >= 'A') {
                            }
                            charAt = (char) (charAt + 26);
                        }
                    }
                    sb.append(charAt);
                } else {
                    charAt = (char) (charAt - 21);
                    if (charAt >= 'a') {
                        sb.append(charAt);
                    }
                    charAt = (char) (charAt + 26);
                    sb.append(charAt);
                }
            }
            this.f103t1.setText(sb.toString());
            return;
        }
        Toast.makeText(getApplicationContext(), "Try again", 1).show();
    }
}

우리가 입력하는 값과 ((i*2)+4)이 같은지 비교한다. 같을 경우 flag를 출력하고 다를 경우 Try again을 출력한다. 여기서 i는 random으로 생성된다.

 

그럼 우리는 flag를 얻을 수 있는 2가지 방법을 떠올릴 수 있다.

  • random() 함수에 후킹을 걸어 random값 파악 혹은 리턴값 설정
  • check() 함수에 후킹을 걸어 전달 인자 파악

 

우선, 첫 번째 방법이다.

Java.perform(function() {

  var a = Java.use("com.ad2001.frida0x1.MainActivity");
  a.get_random.implementation = function(){

    console.log("This method is hooked");
    var ret_val = this.get_random();
    console.log("The return value is " + ret_val);
    console.log("The value to bypass the check " + (ret_val * 2 + 4 )) // To bypass the check
    return ret_val; //returning the original random value from the get_random method

  }

})

코드는 fridalab에서 제공한 writeup 코드다. 직접 짜긴했었는데, 이게 더 깔끔해서 이걸 그대로 사용한다. 나머지 문제들도 그럴듯.

 

코드를 해석해보겠다. `a.get_random.implementation`을 통해 get_random 함수가 실행될 때 수행할 함수를 정의하고 있다.

`this.get_random`을 통해 원래 함수를 실행시키고 그 결과값을 ret_val에 저장하여 사용하고 있다.

 

random값이 onCreate()에 의해 바로 생성되므로 `-l` 옵션으로 스크립트를 바로 로드하도록 구성해야한다.

frida -U -f com.ad2001.frida0x1 -l .\jwcs_frida_1.js

이런 식으로 실행하면

실행 결과

랜덤 값과, flag를 얻기 위해 계산된 값을 볼 수 있다.

flag

짜잔

 

두 번째 방법에 대해 알아보겠다.

Java.perform(function() {
  var a = Java.use("com.ad2001.frida0x1.MainActivity");
  a.check.overload('int', 'int').implementation = function(a, b) {
    // The function takes two arguments; check(random, input)
    console.log("The random number is " + a);
    console.log("The user input is " + b);
    this.check(a, b); // Call the check() function with the correct arguments
  }
});

여기서 overload('int', 'int')를 통해 같은 이름의 다른 메서드를 구분할 수 있다.

check() 함수에서는 random값과 우리가 입력한 값, 총 2개의 인자를 받는다. 이것을 a,b에 저장하여 사용할 수 있다.

 

실행 결과

위와 같이 18이 랜덤값임을 알 수 있다.

flag

짜잔

 

[0x2 문제]

/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {

    /* renamed from: t1 */
    static TextView f103t1;

    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(C0497R.layout.activity_main);
        f103t1 = (TextView) findViewById(C0497R.id.textview);
    }

    public static void get_flag(int a) {
        if (a == 4919) {
            try {
                SecretKeySpec secretKeySpec = new SecretKeySpec("HILLBILLWILLBINN".getBytes(), "AES");
                Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                IvParameterSpec iv = new IvParameterSpec(new byte[16]);
                cipher.init(2, secretKeySpec, iv);
                byte[] decryptedBytes = cipher.doFinal(Base64.decode("q7mBQegjhpfIAr0OgfLvH0t/D0Xi0ieG0vd+8ZVW+b4=", 0));
                String decryptedText = new String(decryptedBytes);
                f103t1.setText(decryptedText);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

MainActivity에서 flag를 얻기 위해서 전달되는 인자의 값이 4919이어야 함을 get_flag() 함수에서 확인할 수 있다. 

 

Java.perform(function() {

    var a = Java.use("com.ad2001.frida0x2.MainActivity");
    a.get_flag(4919);  // method name

})

따라서 위와 같이 코드를 짤 수 있다. 이전에 `<class>.<function>.implementation()`은 수행할 함수를 재정의 했다. 비슷하게 생긴 `a.get_flag(4919)`를 볼 수 있는데, 이건 메서드를 실행시키는 것이다.

exploit

 

flag

짜잔

 

[0x3 문제]

/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {

    /* renamed from: t1 */
    TextView f103t1;

    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(C0498R.layout.activity_main);
        Button btn = (Button) findViewById(C0498R.id.button);
        this.f103t1 = (TextView) findViewById(C0498R.id.textView);
        btn.setOnClickListener(new View.OnClickListener() { // from class: com.ad2001.frida0x3.MainActivity.1
            @Override // android.view.View.OnClickListener
            public void onClick(View v) {
                if (Checker.code == 512) {
                    byte[] bArr = new byte[0];
                    Toast.makeText(MainActivity.this.getApplicationContext(), "YOU WON!!!", 1).show();
                    byte[] KeyData = "glass123".getBytes();
                    SecretKeySpec KS = new SecretKeySpec(KeyData, "Blowfish");
                    byte[] ecryptedtexttobytes = Base64.getDecoder().decode("MKxsZsY9Usw3ozXKKzTF0ymIaC8rs0AY74GnaKqkUrk=");
                    try {
                        Cipher cipher = Cipher.getInstance("Blowfish");
                        cipher.init(2, KS);
                        byte[] decrypted = cipher.doFinal(ecryptedtexttobytes);
                        String decryptedString = new String(decrypted, Charset.forName("UTF-8"));
                        MainActivity.this.f103t1.setText(decryptedString);
                        return;
                    } catch (InvalidKeyException e) {
                        throw new RuntimeException(e);
                    } catch (NoSuchAlgorithmException e2) {
                        throw new RuntimeException(e2);
                    } catch (BadPaddingException e3) {
                        throw new RuntimeException(e3);
                    } catch (IllegalBlockSizeException e4) {
                        throw new RuntimeException(e4);
                    } catch (NoSuchPaddingException e5) {
                        throw new RuntimeException(e5);
                    }
                }
                Toast.makeText(MainActivity.this.getApplicationContext(), "TRY AGAIN", 1).show();
            }
        });
    }
}

MainActivity에서 Check.code가 512면 `YOU WON`과 함께 flag를 얻을 수 있을 것으로 예상된다.

 

package com.ad2001.frida0x3;

/* loaded from: classes3.dex */
public class Checker {
    static int code = 0;

    public static void increase() {
        code += 2;
    }
}

Check 클래스의 코드는 위와 같다. 우리는 code의 값을 512로 맞춰주면 flag를 얻을 수 있을 것이다.

 

<class_reference>.<variable>.value = <value>;

위 문법으로 변수의 값을 설정할 수 있다.

 

Java.perform(function(){

    var a = Java.use("com.ad2001.frida0x3.Checker");
    a.code.value = 512;
    console.log("[*] Check.code의 값을 512로 변경");
})

따라서 우리는 위와 같은 스크립트를 짤 수 있다.

exploit
flag

짜잔

 

[0x4 문제]

package com.ad2001.frida0x4;

/* loaded from: classes3.dex */
public class Check {
    public String get_flag(int a) {
        if (a == 1337) {
            byte[] decoded = new byte["I]FKNtW@]JKPFA\\[NALJr".getBytes().length];
            for (int i = 0; i < "I]FKNtW@]JKPFA\\[NALJr".getBytes().length; i++) {
                decoded[i] = (byte) ("I]FKNtW@]JKPFA\\[NALJr".getBytes()[i] ^ 15);
            }
            return new String(decoded);
        }
        return "";
    }
}

위는 Check 클래스이다. 여기서 flag를 얻을 수 있는 것을 알 수 있다. 여기서 get_flat() 메서드는 static 메서드가 아니기 때문에 인스턴스를 생성해주어야 한다.

 

앞서 0x3문제에서는 별도로 인스턴스를 생성하지는 않았다. 이는 변수가 class variable이기 때문에(static 키워드 사용) 클래스가 로드될 때 생성되어 별도로 인스턴스를 생성하지 않아도 된다. 이에 대한 개념은 아래 참고 자료를 확인하자.

https://dkswnkk.tistory.com/444

 

[Java] static 과 instance의 차이

서론 static과 instance의 차이에 대해서 한번 정리하여 짚고 넘어 갈려고 합니다. 목차 1. 클래스(static) 변수와 인스턴스 변수의 차이 2. 클래스(static) 메서드와 인스턴스 메서드 차이 3. 클래스(static)

dkswnkk.tistory.com

 

따라서 인스턴스를 생성해서 get_flag()에 인자로 1337을 전달해주면 flag를 얻을 수 있다.

Java.perform(function() {

  var check = Java.use("com.ad2001.frida0x4.Check");
  var check_obj = check.$new(); // Class Object
  var res = check_obj.get_flag(1337); // Calling the method
  console.log("FLAG " + res);

})

 

flag

짜잔

 

[0x5 문제]

package com.ad2001.frida0x5;

import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {

    /* renamed from: t1 */
    TextView f103t1;

    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(C0497R.layout.activity_main);
        this.f103t1 = (TextView) findViewById(C0497R.id.textview);
    }

    public void flag(int code) {
        if (code == 1337) {
            try {
                SecretKeySpec secretKeySpec = new SecretKeySpec("WILLIWOMNKESAWEL".getBytes(), "AES");
                Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                IvParameterSpec iv = new IvParameterSpec(new byte[16]);
                cipher.init(2, secretKeySpec, iv);
                byte[] decodedEnc = Base64.getDecoder().decode("2Y2YINP9PtJCS/7oq189VzFynmpG8swQDmH4IC9wKAY=");
                byte[] decryptedBytes = cipher.doFinal(decodedEnc);
                String decryptedText = new String(decryptedBytes);
                this.f103t1.setText(decryptedText);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

MainActivity에 대한 내용이다. 메서드에 대해서도 클래스 메서드, 인스턴스 메서드가 있다. 앞선 문제들은 클래스 메서드였기 때문에 별도로 인스턴스를 생성해주지 않았지만, 해당 문제는 인스턴스 메서드이기 때문에 인스턴스를 생성해주어야 한다.

 

Java.perform(function() {

  var a = Java.use("com.ad2001.frida0x5.MainActivity");
  var main_act = a.$new(); // Class Object
  main_act.flag(1337); // Calling the method

})

위와 같이 수행을 했는데

error

위와 같이 에러가 발생한다.

 

Frida를 통해 직접 인스턴스화한 경우, 안드로이드의 라이프사이클과 스레드 규칙때문에 문제가 발생할 수 있다. Activity와 같은 Android Component는 application context가 필요하다. frida로 생성한 activity 인스턴스는 Android 시스템이 관리하는 라이프사이클을 거치지 않고 직접 인스턴스를 생성하기 때문에 필수적인 초기화 과정이 생략된다 (지피티 피셜).

 

Android 애플리케이션이 시작되면, 시스템은 `AndroidManifest.xml`에 지정된 MainActivity의 인스턴스를 생성한다. MainActivity 인스턴스 생성 과정은 안드로이드 애플리케이션 라이프사이클의 일부이다. 따라서 이미 생성된 인스턴스를 후킹하면 플래그를 얻을 수 있다.

 

Java.performNow(function() {
  Java.choose('com.ad2001.frida0x5.MainActivity', {
      onMatch: function(instance) { // "instance" is the instance for the MainActivity
        console.log("Instance found");
        instance.flag(1337); // Calling the function
    },
    onComplete: function() {}
  });
});
  • Java.performNow(fn){}: 현재 스레드가 Java VM에 연결하며, 함수(fn)를 호출한다. Java.perform은 클래스 로더(class loader)가 아직 사용 불가능한 경우, 함수 호출을 지연시키기 때문에 앱의 클래스에 접근할 필요가 없을 때 Java.performNow()가 더 효율적이다.
  • Java.choose(className, callbacks): Java의 힙 메모리를 스캔하여 실행 중인 특정 클래스(className)의 모든 인스턴스를 열거한다.
    •  onMatch: Java 힙에서 해당 클래스의 인스턴스를 찾으면 호출된다. 
    •  onComplete: 모든 인스턴스를 탐색한 후 호출된다. 탐색이 끝났음을 알리는 역할을 한다.

https://frida.re/docs/javascript-api/

 

JavaScript API

Observe and reprogram running programs on Windows, macOS, GNU/Linux, iOS, watchOS, tvOS, Android, FreeBSD, and QNX

frida.re

exploit
flag

짜잔

[0x6 문제]

package com.ad2001.frida0x6;

import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {

    /* renamed from: t1 */
    TextView f103t1;

    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(C0497R.layout.activity_main);
        this.f103t1 = (TextView) findViewById(C0497R.id.textview);
    }

    public void get_flag(Checker A) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        if (1234 == A.num1 && 4321 == A.num2) {
            Cipher cipher = Cipher.getInstance("AES");
            SecretKeySpec secretKeySpec = new SecretKeySpec("MySecureKey12345".getBytes(), "AES");
            cipher.init(2, secretKeySpec);
            byte[] decryptedBytes = Base64.getDecoder().decode("QQzMj/JNaTblEHnIzgJAQkvWJV2oK9G2/UmrCs85fog=");
            String decrypted = new String(cipher.doFinal(decryptedBytes));
            this.f103t1.setText(decrypted);
        }
    }
}

MainActivity를 확인해보면 get_flag를 실행시키면 flag를 얻을 수 있다. 그런데 Check 클래스의 객체를 인자로 받기 때문에 객체 생성이 필요하다. 

package com.ad2001.frida0x6;

/* loaded from: classes3.dex */
public class Checker {
    int num1;
    int num2;
}

Checker 객체는 이렇게 생겼다. 이에 대한 익스플로잇 코드를 짜면 아래와 같다.

 

Java.perform(function() {
  Java.choose('com.ad2001.frida0x6.MainActivity', {
    onMatch: function(instance) {
      console.log("Instance found");

      var checker = Java.use("com.ad2001.frida0x6.Checker");
      var checker_obj  = checker.$new();  // Class Object
      checker_obj.num1.value = 1234; // num1
      checker_obj.num2.value = 4321; // num2
      instance.get_flag(checker_obj); // invoking the get_flag method

    },
    onComplete: function() {}
  });
});

exploit
flag

 

[0x7 문제]

package com.ad2001.frida0x7;

import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {

    /* renamed from: t1 */
    TextView f103t1;

    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(C0497R.layout.activity_main);
        this.f103t1 = (TextView) findViewById(C0497R.id.textview);
        Checker ch = new Checker(123, 321);
        try {
            flag(ch);
        } catch (InvalidKeyException e) {
            throw new RuntimeException(e);
        } catch (NoSuchAlgorithmException e2) {
            throw new RuntimeException(e2);
        } catch (BadPaddingException e3) {
            throw new RuntimeException(e3);
        } catch (IllegalBlockSizeException e4) {
            throw new RuntimeException(e4);
        } catch (NoSuchPaddingException e5) {
            throw new RuntimeException(e5);
        }
    }

    public void flag(Checker A) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        if (A.num1 > 512 && 512 < A.num2) {
            Cipher cipher = Cipher.getInstance("AES");
            SecretKeySpec secretKeySpec = new SecretKeySpec("MySecureKey12345".getBytes(), "AES");
            cipher.init(2, secretKeySpec);
            byte[] decryptedBytes = Base64.getDecoder().decode("cL/bBqDmfO0IXXJCVFwYLeHp1k3mQr+SP6rlQGUPZTY=");
            String decrypted = new String(cipher.doFinal(decryptedBytes));
            this.f103t1.setText(decrypted);
        }
    }
}

MainActivity에 대한 코드다. flag를 실행시키면 UI에 flag를 출력시키는 것을 확인할 수 있다. 따라서 Java.choose()를 사용해야한다. Checker 클래스를 입력받기 때문에 Checker 클래스를 확인해보자.

 

package com.ad2001.frida0x7;

/* loaded from: classes3.dex */
public class Checker {
    int num1;
    int num2;

    Checker(int a, int b) {
        this.num1 = a;
        this.num2 = b;
    }
}

Checker 클래스에 대한 내용이다. 생성자가 존재하니, 문법을 맞춰서 생성하고 이걸 flag()에 인자로 전달해주면 될것이다.

 

Java.perform(function() {
  Java.choose('com.ad2001.frida0x7.MainActivity', {
    onMatch: function(instance) {
    console.log("Instance found");

    var checker = Java.use("com.ad2001.frida0x7.Checker");
    var checker_obj  = checker.$new(600, 600); // Class Object
    instance.flag(checker_obj); // invoking the get_flag method
  },
    onComplete: function() {}
  });
});

exploit
flag

짜잔

 

이 방법 외에도 생성자를 후킹하는 방법도 있다.

Java.perform(function() {
  var a =  Java.use("com.ad2001.frida0x7.Checker");
  a.$init.implementation = function(param){
    this.$init(600, 600);
  }
});
  • $init: Java의 생성자(Constructor)를 나타낸다. `a.$init.implementation`은 Check 클래스의 생성자를 후킹한다.

onCreate에서 클래스가 호출되므로, `-l` 옵션을 통해 호출하도록 하자.

exploit
flag

짜잔

 

나머지 8~B까지의 문제는 리버싱 능력이 필요하다. 이건 나중에 시간이 된다면 포스팅하도록 하겠다.

728x90
반응형