FridaLab은 프리다를 쉽게 연습할 수 있도록 Ross Marks가 만든 앱이다.
총 8개의 문제로 구성되어 있는데, 지금까지 배웠던 자바스크립트 API를 프리다를 통해 앱 프로세스에 삽입하여 해결하면 된다.
http://rossmarks.uk/blog/fridalab
위 링크를 통해 FridaLab 앱 apk 파일을 다운로드 받는다.
adb install
명령어를 통해 안드로이드 기기에 apk 파일을 설치할 수 있다. 현재 녹스 앱플레이어를 사용 중이므로 nox_adb install
명령어를 사용하겠다.
앱 설치 후 실행하면 위와 같은 화면이 나오는데, 가운데 CHECK 버튼을 클릭하여 문제가 해결되었는지 알 수가 있다.
해당 앱의 패키지 이름을 알아야 프리다를 사용할 수 있으므로 아래와 같이 앱 패키지 이름을 알아낸다. 패키지 이름은 uk.rossmarks.fridalab
이다.
프리다를 사용하여 자바스크립트를 앱 프로세스에 삽입하기 위해서는 앱에서 사용하는 클래스, 메소드 등을 알아야 한다. 그러기 위해서는 apk 파일을 디컴파일하여 자바 소스 코드로 변환해야 한다. 디컴파일 방법은 여러가지가 있지만, 그 중에서도 jadx
라는 프로그램을 사용하여 디컴파일을 진행하겠다.
https://github.com/skylot/jadx/releases
위 링크에서 jadx-gui-1.1.0-no-jre.exe
를 다운로드 받을 수 있다. jadx
는 CLI 방식과 GUI 방식 둘 다 지원하기 때문에 보기 편한 GUI 방식을 사용하기로 했다.
프로그램 실행 후, FridaLab apk 파일을 로드하면 캡처화면과 같이 디컴파일하여 보여준다.
준비가 다 되었으므로 이제부터 문제 1번부터 8번까지 차례대로 풀어보도록 하자.
1번 문제는 challenge_01
클래스의 변수인 chall01
의 값을 1로 변경하면 된다.
challenge_01
클래스를 살펴보면 static 변수인 chall01
이 존재함을 알 수 있다. 자바스크립트 코드를 삽입하여 이 값을 1로 변경해보자.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// Challenge 01
var chall_01 = Java.use("uk.rossmarks.fridalab.challenge_01");
chall_01.chall01.value = 1;
console.log("\nchallenge_01 solved!");
});
});
자바스크립트 코드를 프리다를 통해 FridaLab 앱 프로세스에 삽입한 후 앱 화면에서 CHECK 버튼을 클릭하면 1번 문제를 해결했다는 표시가 나온다.
2번 문제는 Chall02()
라는 메소드를 실행하면 된다.
Chall02()
라는 메소드가 어떤 클래스에 존재하는지 확인하기 위해 jadx
에서 검색 한다. 검색하면 MainActivity
클래스 내에 존재하는 것을 알 수 있는데, static
키워드가 없는 것으로 봐서 instance
메소드 임을 알 수 있다. instance
메소드는 Java.choose()
함수로 가져 올 수 있다.
chall02()
메소드를 실행하면 completeArr[1]
에 1이라는 값이 들어가도록 되어 있다. 이제 메소드를 실행하는 자바스크립트 코드를 작성해 보자. 이전 문제에서 작성했던 코드에 이어서 작성할 것이다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// Challenge 01 (생략)
// Challenge 02
Java.choose("uk.rossmarks.fridalab.MainActivity", {
"onMatch": function(chall_02) {
chall_02.chall02();
},
"onComplete": function() {
console.log("\nchallenge_02 solved!");
}
})
});
});
마찬가지로 CHECK 버튼 클릭시 2번 문제까지 해결되었다는 표시가 나오게 된다.
static
메소드를 호출할 때는Java.use()
,
instance
메소드를 호출할 때는Java.choose()
함수를 통해 호출할 수 있다.
chall03()
메소드가 true 값을 반환하도록 만들면 된다.
chall03()
메소드를 검색하여 어떤 로직을 구성하고 있는지 확인한다.
chall03()
메소드는 false 값을 리턴하는 로직을 가진 메소드임을 확인할 수 있다. 이 메소드를 true 값을 리턴하도록 재작성한다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// Challenge 01 (생략)
// Challenge 02 (생략)
// Challenge 03
var chall_03 = Java.use("uk.rossmarks.fridalab.MainActivity");
chall_03.chall03.implementation = function() {
console.log("\nchallenge_03 solved!");
return true;
}
});
});
chall03()
메소드는 재작성을 통해 true 값을 리턴하도록 스크립트를 인젝션했으므로, CHECK 버튼을 눌러야 호출되어 해결되었다는 로그가 출력된다.
chall04()
메소드 인자에 frida
라는 문자열을 전달하면 된다.
마찬가지로 chall04()
메소드를 검색하여 로직을 확인해본다.
해당 메소드는 str
이라는 인자에 frida
문자열이 전달되면 completeArr[3]
에 1이라는 값이 저장되는 로직임을 알 수 있다. 따라서 메소드를 호출할 때 frida
문자열을 전달하도록 스크립트를 작성하면 해결할 수 있음을 알 수 있다. instance
메소드를 호출하므로 Java.choose
함수를 호출하면 된다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// Challenge 01 (생략)
// Challenge 02 (생략)
// Challenge 03 (생략)
// Challenge 04
Java.choose("uk.rossmarks.fridalab.MainActivity", {
"onMatch": function(chall_04) {
chall_04.chall04("frida");
},
"onComplete": function() {
console.log("\nChallenge_04 Solved!");
}
});
});
});
FridaLab 화면을 보면 chall04()
메소드가 호출되었음을 알 수 있다.
4번 문제와 유사하게 chall05()
메소드의 인자에 frida
문자열을 전송하는데, chall04()
메소드는 앱 프로세스에 접근했을 때 처음 한 번만 호출하는 것과 다르게 chall05
메소드는 CHECK 버튼을 클릭할 때마다 항상 전송하도록 해야 한다.
일단 chall05()
메소드의 로직을 확인해보자.
this
키워드를 사용하여 chall05()
메소드 자신을 호출할 때 frida
문자열을 인자에 전달하도록 로직을 재작성할 것이다. 이번에는 chall05()
라는 메소드가 매개변수 정보가 다른 동일한 이름의 메소드가 존재한다고 가정하여 오버로딩을 위해 overload
를 사용해보겠다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// Challenge 01 (생략)
// Challenge 02 (생략)
// Challenge 03 (생략)
// Challenge 04 (생략)
// Challenge 05
var chall_05 = Java.use("uk.rossmarks.fridalab.MainActivity");
chall_05.chall05.overload("java.lang.String").implementation = function(arg) {
this.chall05("frida");
console.log("\nChallenge_05 Solved!");
}
})
})
3번 문제와 유사하게 CHECK 버튼을 클릭할 때마다 5번 문제가 해결되었다는 로그가 출력된다.
6번 문제는 올바른 값으로 10초 후 chall06()
메소드를 호출하면 된다.
chall06()
메소드의 로직을 확인해보자.
challenge_06
클래스의 confirmChall06
메소드를 호출하여 참일 경우 정답으로 처리하는 것을 알 수 있다. 메소드 이름을 ctrl + 클릭
하여 해당 메소드 정의 부분으로 이동할 수 있다.
메소드를 확인해 보니 두 가지 조건을 만족해야 하는데, 첫 번째 조건은 i
값과 chall06
값이 일치해야 하고, 두 번째 조건은 startTime()
메소드를 호출한 시간 값을 timeStart
에 저장하고 이 시간 이후로 10초가 지나야 참이 됨을 알 수 있다.
일단 두 번째 조건부터 해결해보도록 하자. setTimeout()
이라는 함수를 통해 일정 시간이 지난 후 콜백 함수를 호출 할 수 있다. 콜백 함수를 감싸기 위해 지금까지 작성했던 setImmediate()
함수 외부에서 작성할 것이다.
setTimeout(function, delay)
delay 값에 전달되는 시간만큼 지연된 후 function 콜백 함수를 호출한다.
// fridalab.js
setImmediate(function() {
// 생략
})
// Challenge_06
setTimeout(function() {
console.log("\nChallenge_06 10 seconds!");
}, 10000);
스크립트 코드를 앱 프로세스에 삽입하면 10초 후에 로그가 출력된다. 두 번째 조건이 참이 되도록 스크립트를 작성했으므로, 이제는 첫 번째 조건을 만족시키도록 작성해보자. chall06
변수의 값은 addChall06()
메소드가 호출될 때 인자에 전달되는 값을 누적하다가 그 값이 9000을 초과해야 i
값과 일치하는 것을 알 수 있다.
addChall06()
메소드가 호출되는 부분을 보면 랜덤한 값이 인자로 전달 됨을 알 수 있다. 이 값을confirmChall06()
메소드가 호출될 때 인자로 전달하면 첫 번째 조건을 만족시킬 수 있다.
// fridalab.js
setImmediate(function() {
// 생략
})
// Challenge_06
setTimeout(function() {
console.log("\nChallenge_06 10 seconds!");
setImmediate(function() {
Java.perform(function() {
var chall_06 = Java.use("uk.rossmarks.fridalab.challenge_06");
chall_06.addChall06.overload("int").implementation = function(arg) {
Java.choose("uk.rossmarks.fridalab.MainActivity", {
"onMatch": function(instance) {
instance.chall06(chall_06.chall06.value);
},
"onComplete": function() {
console.log("\nChallenge_06 Solved!");
}
})
}
})
})
}, 10000);
정리하면, challenge_06
클래스를 chall_06
변수에 넣어 해당 클래스 내에 있는 addChall06()
메소드를 재작성하였다. addChall06()
메소드는 MainActivity
클래스 내에서 호출할 때 chall06
값을 결정하므로 이 클래스의 인스턴스를 가져와 정답을 체크하는 chall06()
메소드를 호출한다. 이 때 addChall06()
메소드를 호출하면서 challenge_06
클래스에 정의 된 chall06
변수의 값을 인자로 전달하면 이 값이 confirmChall06()
메소드의 인자로 전달되고, 첫 번째 조건인 i == chall06
을 만족시킬 수 있다.
chall07Pin()
메소드를 브루트포스하여 chall07()
메소드로 확인하면 된다. 브루트포스(Brute Force)는 무작위 공격을 의미하며, 전체 경우의 수를 다 대입하는 것을 말한다.
chall07()
메소드의 내용을 확인해 보자.
인자로 전달된 str
값을 challenge_07
클래스의 check07Pin()
메소드를 호출할 때 인자로 전달했을 때 참이면 정답으로 처리함을 알 수 있다. check07Pin()
메소드를 ctrl + 클릭
하여 정의 부분으로 이동한다.
인자로 전달된 str
값과 chall07
이라는 변수를 비교하여 같으면 참을 반환함을 알 수 있는데, chall07
값을 결정하는 setChall07()
메소드가 어디서 호출하는지 검색해보자.
MainActivity
에서 호출되는 것을 알 수 있고, 그 값은 1000 ~ 9999 중 랜덤한 값으로 결정된다. 따라서 이 범위로 브루트포스하여 어떤 값이 저장된 건지 확인하여야 한다. 그 값을 찾은 후에, MainActivity
인스턴스를 가져와 chall07()
메소드를 호출할 때 전달할 것이다.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// Challenge 01 (생략)
// Challenge 02 (생략)
// Challenge 03 (생략)
// Challenge 04 (생략)
// Challenge 05 (생략)
// Challenge 07
var chall_07 = Java.use("uk.rossmarks.fridalab.challenge_07");
Java.choose("uk.rossmarks.fridalab.MainActivity", {
"onMatch": function(instance) {
for(var i=1000; i<=9999; i++) {
var str_i = String(i)
if(chall_07.check07Pin(str_i)) {
instance.chall07(str_i);
break;
}
}
},
"onComplete": function() {
console.log("\nChallenge_07 Solved!");
}
})
})
})
// Challenge_06 (생략)
chall_07
변수에 challenge_07
클래스를 저장하고, MainActivity
의 인스턴스를 가져와 브루트포스를 진행한다. for문을 사용하여 1000 ~ 9999까지의 값을 String 형으로 변환하여 check07Pin()
메소드의 인자 값으로 전달하면 참을 반환했을 때가 chll07
의 값이다. 따라서, 이 값을 MainActivity
인스턴스의 chall07()
메소드를 호출할 때 인자 값으로 전달하면 해결할 수 있다.
FridaLab을 실행하고 CHECK 버튼을 클릭했을 때, CHECK 버튼의 텍스트를 Confirm으로 변경하면 된다.
chall08()
메소드의 정의 부분을 확인해 보자.
findViewById()
메소드를 통해 CHECK 버튼 인스턴스를 찾아, 텍스트 값이 Confirm
이면 참을 반환하도록 되어 있다. 인스턴스를 찾을 때 R.id.check
값을 인자로 보내는데 마찬가지로 정의 부분으로 이동해서 그 값을 확인해보자.
정수형 값을 가지고 있음을 알 수 있다. MainActivity
인스턴스를 가져와 마찬가지로 CHECK 버튼의 텍스트 값을 Confirm으로 변경하는 스크립트를 삽입하도록 하자.
// fridalab.js
setImmediate(function() {
Java.perform(function() {
// Challenge 01 (생략)
// Challenge 02 (생략)
// Challenge 03 (생략)
// Challenge 04 (생략)
// Challenge 05 (생략)
// Challenge 07 (생략)
// Challenge 08
var klass = Java.use("android.widget.Button");
Java.choose("uk.rossmarks.fridalab.MainActivity", {
"onMatch": function(instance) {
var checkId = instance.findViewById(2131165231);
var check = Java.cast(checkId, klass);
var string = Java.use("java.lang.String");
check.setText(string.$new("Confirm"));
},
"onComplete": function() {
console.log("\nChallenge_08 Solved!");
}
})
})
})
// Challenge_06 (생략)
먼저 Java.cast()
라는 함수가 등장했는데, 이 함수는 앞에서 chall08()
메소드의 정의 부분을 봤을 때, findViewById()
메소드로 가져온 인스턴스를 Button
클래스로 형변환 하는 것을 구현한 것이라고 보면 된다.
Java.cast(handle, klass)
handle에 전달된 인스턴스를 klass에 전달된 클래스로 캐스팅하는 함수이다.
MainActivity
인스턴스를 가지고 와서 findViewById()
메소드를 호출하는데, 이 때 위에서 확인했던 R.id.check
값을 인자로 전달한다. checkId
변수에 들어가 있는 값을 Button
클래스로 캐스팅하고, setText()
메소드를 통해 텍스트 값을 Confirm
으로 변경한다. setText()
메소드에 문자열을 전송할 때는 java.lang.String
클래스의 인스턴스를 전달해야 하는데, 이 때 $new()
메소드로 Confirm
이라는 문자열 값을 가지는 인스턴스를 생성하여 전달한다. 스크립트 인젝션 후, CHECK 버튼을 클릭하면 위 캡처화면처럼 텍스트가 변경됨을 확인할 수 있다.