FridaLab은 APK 분석과 Frida를 이용하여 간단하게 앱 분석을 연습할 수 있도록 만들어진 애플리케이션입니다.
1번부터 8번까지의 문제가 있으며 우리의 목표는 문제의 지시에 따라 FridaLab 앱을 직접 분석하여, Frida를 사용해서 앱이 우리가 원하는 동작을 수행하도록 하는 것입니다.
위 링크에 들어가 다운로드 버튼을 누르면 바로 FridaLab.apk 파일이 다운로드 됩니다.
이 파일을 녹스 앱플레이어에 드래그 앤 드롭하면 간단하게 설치할 수 있습니다.
앞서 애플리케이션을 직접 분석해야 한다고 했는데, 이를 위해서는 앱이 어떤 소스코드로 짜여져 있는지 확인해야 합니다.
따라서 완성된 APK 파일을 디컴파일하는 작업이 필요합니다.
이 디컴파일 작업을 도와주는 도구가 바로 Jadx입니다.
위 링크에 들어가 설치 파일을 다운로드합니다.
압축을 푼 뒤 폴더에 들어가보면 jadx-gui가 설치된 것을 확인할 수 있습니다.
그럼 이제 본격적으로 문제를 풀어보도록 하겠습니다.
우선 cmd에 nox_adb shell "/data/local/tmp/frida-server &"
명령어로 frida-server를 실행합니다.
그리고 Jadx를 실행한 뒤 설치했던 FridaLab.apk 파일을 열어줍니다.
우리가 분석할 클래스들은 사진에 보이는 클래스 파일들입니다.
이제 녹스에서 FridaLab 앱을 실행하고 본격적으로 해킹을 시작해봅시다.
1. Change class challenge_01's variable 'chall01' to: 1
우선 적당한 곳에 fridalab 폴더를 생성하고 그 밑에 fridalab.js 파일을 만들어 후킹 코드를 작성하겠습니다. 에디터는 vscode를 사용하겠습니다.
첫 번째 문제를 해석해보면 challenge_01
클래스의 chall01
변수의 값을 1로 바꾸라고 합니다.
Jadx에서 challenge_01 클래스를 찾아보면 소스코드를 알 수 있습니다.
// uk.rossmarks.fridalab.challenge_01
package uk.rossmarks.fridalab;
/* loaded from: classes.dex */
public class challenge_01 {
static int chall01;
public static int getChall01Int() {
return chall01;
}
}
challenge_01
클래스 안에 static으로 chall01
변수가 선언된 것을 볼 수 있습니다. 그리고 밑에 chall01
변수의 값을 가져오는 함수가 있네요.
그럼 문제에서 지시한대로 chall01
변수의 값을 1로 바꾸는 후킹 코드를 작성해보겠습니다.
// fridalab.js
setImmediate(function() {
// challenge 1
Java.perform(function() {
let challenge_01 = Java.use('uk.rossmarks.fridalab.challenge_01');
challenge_01.chall01.value = 1;
console.log('Challenge 1 clear!');
});
});
천천히 위 코드를 설명하겠습니다.
setImmediate()
함수
setImmediate()
함수를 사용한 이유는 Frida는 장치가 느려질 때 자동으로 프로세스를 종료하려고 하는데 해당 함수를 사용하면 백그라운드에서 자동으로 스크립트가 재실행돼서 종료되는 것을 막을 수 있습니다.
Java.perform()
함수
후킹 코드는 기본적으로 Java.perform()
함수 안에 실행할 함수를 파라미터로 넘겨서 사용합니다.
값 변경 작업
Java.use()
함수를 통해 fridalab의 challenge_01
클래스를 가져온 뒤 challenge_01
변수에 저장합니다. (Java.use(앱의 패키지명.클래스 이름)
으로 클래스를 가져올 수 있습니다.) 그리고 challenge_01
변수 안에 들어있는 chall01
변수의 값을 value를 통해 접근하여 1로 바꾸고 있습니다.
변경이 완료되면 클리어 메시지를 출력합니다.
이제 작성한 후킹 스크립트를 FridaLab 앱 프로세스에 삽입해봅시다.
다음 명령어를 통해 작업을 수행합니다.
> frida -U FridaLab -l [스크립트 파일 위치]
C:\Users\dev>frida -U FridaLab -l "C:\Users\dev\fridalab\fridalab.js"
____
/ _ | Frida 16.0.19 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to SM-N976N (id=127.0.0.1:62001)
Attaching...
Challenge 1 clear!
다시 문제로 돌아가서 check버튼을 눌러보면 문제가 해결된 것을 알 수 있습니다.
2. Run chall02()
chall02()
함수를 실행하라고 하네요. 일단 chall02()
함수가 어디에 있는지 알 수 없으니 Jadx에서 검색 기능을 이용하겠습니다.
사실 이렇게 작은 소스코드를 분석할 때는 검색 기능을 사용하지 않고 그냥 찾아도 됩니다. 하지만 큰 소스코드를 분석할 때 검색 기능을 사용하지 않는다면 모래사장에서 바늘 찾기입니다.
MainActivity
클래스에서 선언되었네요.
// uk.rossmarks.fridalab.MainActivity
private void chall02() {
this.completeArr[1] = 1;
}
chall02()
함수는 static으로 선언되어 있지 않기 때문에 Java.use()
함수로 가져올 수 없습니다.
static으로 함수를 선언하면 함수가 메모리에 올라갈 때 정적 함수를 자동으로 생성해주기 때문에 Java.use()
함수로 메모리에 있는 정적 함수를 가져올 수 있지만, static으로 선언하지 않으면 정적 함수가 자동으로 생성되지 않기 때문에, 이 함수를 실행하려면 이 함수를 가지고 있는 인스턴스를 직접 찾아서 함수를 실행 시켜줘야 합니다.
Java.choose()
함수를 이용하면 해당 작업을 진행할 수 있습니다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// challenge 1 (생략)
// challenge 2
Java.choose('uk.rossmarks.fridalab.MainActivity', {
onMatch: function(instance) {
instance.chall02();
},
onComplete: function() {
console.log('Challenge 2 clear!');
}
});
});
});
Java.choose()
함수
함수의 첫 번째 인자에 찾고자 하는 인스턴스를(MainActivity
클래스), 그리고 두 번째 인자에는 인스턴스를 찾았을 때(onMatch)와 찾기를 끝냈을 때(onComplete) 실행할 함수들에 대해 정의할 수 있습니다.
onMatch
함수
인스턴스를 찾았을 경우 해당 인스턴스를 인자로 받습니다. 그 다음 인스턴스 안에 있는 chall02
함수를 실행하고 있습니다.
onComplete
함수
인스턴스 찾기가 완료되었을 경우 클리어 메시지를 출력하고 있습니다.
파일을 실행한 뒤 다시 앱으로 돌아가 check 버튼을 누르면 해결된 것을 볼 수 있습니다.
3. Make chall03() return true
이번에는 chall03()
함수가 true 값을 반환하도록 하라고 하네요.
chall03()
함수도 MainActivity
클래스에서 선언되고 있습니다.
// uk.rossmarks.fridalab.MainActivity
public boolean chall03() {
return false;
}
현재는 함수가 false를 반환하고 있는데, chall03()
함수가 true를 반환하도록 함수를 수정하는 코드를 작성해봅시다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// challenge 1 (생략)
// challenge 2 (생략)
// challenge 3
let challenge_03 = Java.use('uk.rossmarks.fridalab.MainActivity');
challenge_03.chall03.implementation = function() {
return true;
};
console.log('Challenge 3 clear!');
});
});
일단 MainActivity
클래스를 가져온 뒤 클래스 안에 있는 chall03()
함수를 implementation
을 통해 새로운 함수로 정의하고 있습니다. true값을 반환하는 함수네요.
코드를 실행한 뒤 FridaLab 앱에서 문제가 해결된 것을 볼 수 있습니다.
4. Send "frida" to chall04()
이번에는 chall04()
함수를 호출할 때 "frida" 값을 인자로 보내라고 하네요.
// uk.rossmarks.fridalab.MainActivity
public void chall04(String str) {
if (str.equals("frida")) {
this.completeArr[3] = 1;
}
}
str
이라는 문자열을 인자로 받아 str
의 값이 "frida" 와 같은지 확인하고 있네요.
2번 문제와 마찬가지로 Java.choose()
함수로 인스턴스를 찾아 함수를 호출할 것인데, 함수를 호출할 때 "frida" 인자값과 함께 호출해주면 됩니다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// challenge 1 (생략)
// challenge 2 (생략)
// challenge 3 (생략)
// challenge 4
Java.choose('uk.rossmarks.fridalab.MainActivity', {
onMatch: function(instance) {
instance.chall04("frida");
},
onComplete: function() {
console.log('Challenge 4 clear!');
}
});
});
});
코드를 실행한 뒤 앱으로 돌아가면 문제가 해결된 것을 볼 수 있습니다.
5. Always send "frida" to chall05()
이번 문제를 해석해보면 chall05()
함수에 항상 "frida" 인자를 보내라고 하네요.
문제만 읽었을때는 무슨 말인지 잘 모르겠으니 직접 코드를 분석해봅시다.
일단 chall05()
함수는 MainActivity
클래스 안에 있다는 것을 알 수 있습니다.
// uk.rossmarks.fridalab.MainActivity
public void chall05(String str) {
if (str.equals("frida")) {
this.completeArr[4] = 1;
} else {
this.completeArr[4] = 0;
}
}
str
을 인자로 받아 값이 "frida" 이면 해결, 아니면 해결되지 않은것으로 처리하네요. (completeArr
변수는 MainActivity
클래스에 선언되어 있는 배열이며 해결된 문제는 1의 값을 가지고 있고 해결되지 않은 문제는 0의 값을 가지고 있습니다.)
그리고 한가지 더 봐야 할 것은 FridaLab 앱에서 check 버튼을 눌렀을 때 실행되는 함수입니다.
40번째 라인을 보시면 chall05()
함수에 "notfrida!" 인자를 보내기 때문에 check 버튼을 클릭할 때 마다 5번째 문제가 해결되지 않게됩니다.
따라서 다음과 같이 후킹 코드를 작성할 수 있습니다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// challenge 1 (생략)
// challenge 2 (생략)
// challenge 3 (생략)
// challenge 4 (생략)
// challenge 5
let challenge_05 = Java.use('uk.rossmarks.fridalab.MainActivity');
challenge_05.chall05.implementation = function(str) {
str = "frida";
this.chall05(str);
};
console.log('Challenge 5 clear!');
});
});
우선 chall05()
함수가 들어있는 MainActivity
함수를 Java.use()
함수를 통해 가져오고, chall05()
함수를 str
을 인자로 받는 함수로 수정합니다.
str
을 인자로 받은 뒤 str
변수의 값을 "frida" 로 변경하여 this(MainActivity
클래스 자신을 의미함)의 chall05()
함수에 변경된 str
을 인자로 넘겨 호출합니다.
이렇게 작성하면 chall05()
함수로 어떤 인자값이 들어오더라도 "frida" 라는 값으로 수정하여 호출되기 때문에 항상 인자값을 "frida"로 고정할 수 있습니다.
5번 문제도 클리어!
6. Run chall06() after 10 seconds with correct value
문제를 해석해보면 chall06()
함수를 10초 뒤에 올바른 값과 함께 실행하라고 하네요. 코드를 분석해보겠습니다.
6번째 문제는 challenge_06
클래스가 따로 존재하네요.
// uk.rossmarks.fridalab.challenge_06
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;
}
}
}
코드를 간단하게 살펴보겠습니다.
startTime()
함수
timeStart
변수의 값을 현재 시간으로 설정합니다.
confirmChall06()
함수
i
를 인자값으로 받은 뒤 i
와 chall06
변수의 값이 같고, 현재 시간이 시작 시간으로부터 10000ms(10초)뒤라면 true를 반환합니다.
addChall06()
함수
i
를 인자값으로 받은 뒤 chall06
변수에 i
를 더하고, 만약 chall06
변수의 값이 9000이 넘는다면 chall06
의 값은 인자로 전달받은 i
의 값이 됩니다.
이제 MainActivity
클래스에 있는 challenge_06
클래스와 관련된 코드를 보겠습니다.
일단 아까 봤던 challenge_06
의 startTime()
함수를 실행합니다.
그리고 addChall06()
함수에 1부터 50까지의 랜덤한 값을 인자로 전달합니다.
그다음 타이머를 설정하여 1초마다 랜덤한 값으로 addChall06()
함수를 실행하네요.
그리고 마지막으로 chall06()
함수에서는 i
를 인자값으로 받아 challenge_06
클래스의 confirmChall06()
함수에 i
값을 넘겨 문제가 해결될지 말지를 결정하네요.
정리하면 10초 뒤에 1초마다 랜덤하게 바뀌는 challenge_06
클래스의 chall06
변수의 값과 같은 값을 chall06()
함수에 전달하여 호출하면 되는 문제입니다.
코드는 다음과 같습니다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// ...
});
});
setTimeout(function() {
// challenge 6
let challenge_06 = Java.use('uk.rossmarks.fridalab.challenge_06');
Java.choose('uk.rossmarks.fridalab.MainActivity', {
onMatch: function(instance) {
instance.chall06(challenge_06.chall06.value);
},
onComplete: function() {}
});
console.log('Challenge 6 clear!');
}, 10000);
setImmediate()
함수 밑의 Java.perform()
함수에서는 지금까지 작성한 1번부터 5번까지의 코드가 순차적으로 실행되고 있으므로, 정확히 10초 뒤에 6번 문제를 풀기 위해서는 setTimeout()
함수를 설정하고 밖으로 빼놓으면 됩니다.
그러면 코드가 실행되었을때부터 10초 뒤에 setTimeout()
함수 안에 있는 코드가 실행됩니다.
MainActivity
클래스의 chall06()
함수에 challenge_06
클래스의 chall06
변수의 값을 전달하여 문제를 해결하였습니다.
7. Bruteforce check07Pin() then confirm with chall07()
문제를 해석하면 check07Pin()
함수를 브루트포스를 통해 알맞은 값을 찾아내어 chall07()
의 인자에 전달해 호출하라네요.
코드를 분석해봅시다.
// uk.rossmarks.fridalab.MainActivity
public void chall07(String str) {
if (challenge_07.check07Pin(str)) {
this.completeArr[6] = 1;
} else {
this.completeArr[6] = 0;
}
}
MainActivity
의 chall07
함수에서 str
을 인자로 받고 challenge_07
클래스의 check07Pin()
함수의 인자로 str
을 전달해서 반환값이 true면 문제가 해결되네요.
그럼 challenge_07
클래스를 살펴보겠습니다.
// uk.rossmarks.fridalab.challenge_07
package uk.rossmarks.fridalab;
/* loaded from: classes.dex */
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);
}
}
setChall07()
함수
1000부터 9999까지의 랜덤한 수를 생성하여 BuildConfig.FLAVOR
(이 변수의 값은 빈 문자열입니다. 보통 숫자 값을 문자열로 바꿀 때 빈 문자열을 더해줍니다.) 문자열과 더한 값을 chall07
변수에 넣습니다.
check07Pin()
함수
인자로 전달받은 값이 chall07
변수의 값과 동일하다면 true를 반환합니다.
그리고 MainActivity
클래스에서 setChall07()
함수를 실행하네요.
check07Pin()
함수에 1000부터 9999까지의 숫자를 넣어서 true가 반환되는 값을 chall07()
함수의 인자로 전달하면 됩니다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// challenge 1 (생략)
// challenge 2 (생략)
// challenge 3 (생략)
// challenge 4 (생략)
// challenge 5 (생략)
// challenge 7
let challenge_07 = Java.use('uk.rossmarks.fridalab.challenge_07');
for(let i=1000; i<10000; i++) {
if(challenge_07.check07Pin(String(i))) {
Java.choose('uk.rossmarks.fridalab.MainActivity', {
onMatch: function(instance) {
instance.chall07(String(i));
},
onComplete: function() {
console.log('Challenge 7 clear!')
}
});
}
}
});
});
challenge_07
클래스에 있는 check07Pin()
함수에 1000부터 9999까지의 값을 대입해보면서 true 값이 반환되면 MainActivity
클래스에 있는 chall07()
함수에 그 값을 전달하고 있습니다.
브루트포스(무차별 대입) 공격으로 문제를 해결했습니다.
8. Change 'check' button's text to 'Confirm'
마지막 문제입니다. check 버튼의 텍스트를 'Confirm' 으로 바꾸라고 하네요.
check 버튼의 텍스트를 수정하기 위해서는 버튼의 id값이 필요합니다. 버튼의 id값은R
클래스 파일에 들어있습니다.
다음은 버튼의 텍스트를 바꾸는 코드입니다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// challenge 1 (생략)
// challenge 2 (생략)
// challenge 3 (생략)
// challenge 4 (생략)
// challenge 5 (생략)
// challenge 7 (생략)
// challenge 8
Java.choose('uk.rossmarks.fridalab.MainActivity', {
onMatch: function(instance) {
let btnClass = Java.use('android.widget.Button');
let id = instance.findViewById(0x7f07002f);
let checkBtn = Java.cast(id, btnClass);
let string = Java.use('java.lang.String');
checkBtn.setText(string.$new('Confirm'));
},
onComplete: function() {
console.log('Challenge 8 clear!');
}
});
});
});
자바에서 버튼 클래스를 btnClass
변수에 담습니다.
그다음 MainActivity
인스턴스에서 check 버튼을 id값으로 가져온 다음 그것을 자바의 버튼 클래스로 캐스팅합니다.
그리고 자바의 문자열 클래스에서 문자열을 생성하고 check 버튼의 텍스트로 지정합니다.
check 버튼의 텍스트가 바뀌어있는것을 확인할 수 있습니다.
모든 문제를 해결했습니다!
이번 글에서는 실제 앱 분석을 위해 FridaLab의 문제들을 풀어보면서 Frida 도구 연습을 진행했습니다. 직접 FridaLab 문제를 풀어보면서 Frida 도구와 어느정도 익숙해졌기를 바랍니다.
다음 글부터는 더 심화된 내용으로 Frida를 실습해보도록 하겠습니다. 깊이 공부할수록 재미있는 것들을 더 많이 할 수 있습니다!