RASP 에 대한 고찰
Runtime Application Self-Protection (RASP)
Introduction
일부 앱에서는 비정상 환경 또는 서비스에 위협이라고 판단되면 서비스를 중단하거나 사용자에게 환경이 위험하다고 알린다.
이러한 방식을 앱 내에서 활성화하여 위협 환경을 앱이 실행될 때 공격을 방지하고 무력화하는 방법을 Runtime Application Self-Protection(RASP) 라고 칭한다.
최근에 비정상 환경에서 앱을 가동하면 탐지하여 앱의 사용을 막는 기능을 확인했다. 그러나 종종 미탐하는 케이스가 발견되어 이를 분석해보았다.
아래를 통해 어떤 형식으로 이를 대응하는지 살펴보겠다.
Understanding the Core Issue
아래 함수는 매개변수로 Activity Context 를 받으면 반환값으로 Boolean 을 반환한다.
안에 로직을 살펴보겠다.
1
2
3
4
5
6
7
8
9
10
11
12
public static boolean g(Activity activity0) {
sc sc0;
AlertDialog.Builder alertDialog$Builder0;
if("1".equals(ic.j(activity0))) {
alertDialog$Builder0 = new AlertDialog.Builder(activity0, 0x7F0D0113)
.setTitle(0x7F0C01D8)
.setMessage(0x7F0C01D7)
.setCancelable(false); // style:Theme.alert
sc0 = (DialogInterface dialogInterface0, int v) -> activity0.finish();
}
{ . . . }
}
"1".equals(ic.j(activity0))
에 조건이 맞으면 AlertDialog 를 띄운다.
ic.j(activity0)
는 결국 반환값이 "1"
이 되어야 한다는 것이다.
ic.j
결과가 "1"
과 같을 경우 AlertDialog 를 띄운다.
j
함수로 넘어가본다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public static String j(Context context0) {
boolean result;
ConfigData configData0 = new ConfigData();
configData0.a();
String[] arr_s = configData0.a().split("\\|");
String[] arr_s1 = configData0.c().split("\\|");
String[] arr_s2 = configData0.b().split("\\|");
boolean res = false;
for(int v1 = 0; true; ++v1) {
result = true;
if(v1 >= arr_s.length) {
break;
}
if(new File(arr_s[v1]).exists()) {
res = true;
}
if(res) {
break;
}
}
if(!res) {
for(int v2 = 0; v2 < arr_s1.length; ++v2) {
res = new File(arr_s1[v2]).isDirectory();
if(res) {
break;
}
}
}
if(!res) {
for(int v = 0; v < arr_s2.length; ++v) {
String s = arr_s2[v];
if(fc.i(context0.getPackageManager(), s) != null) {
return result ? "1" : "0";
}
}
}
result = res;
return result ? "1" : "0";
}
위에 함수를 봐본다.
ConfigData
에 있는 a
, b
, c
의 반환값을 |
을 기준으로 split 하여 변수에 저장한다.
중간에 File class 를 통하여 작업을 처리하는 것을 볼 수 있는데 exists()
와 isDirectory()
를 통하여 검증한다.
그리고 fc.i(context0.getPackageManager(), s)
의 반환값이 null 이 아닐 경우 조건 성립으로 간주한다. `
i
함수로 넘어가본다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static String i(PackageManager packageManager0, String s) {
if(s != null && !"".equals(s)) {
try {
PackageInfo packageInfo0 = packageManager0.getPackageInfo(s, 0);
if(packageInfo0 != null) {
return packageInfo0.versionName;
}
nc.f("fc", "PackageInfo is null: " + s);
}
catch(PackageManager.NameNotFoundException | Exception unused_ex) {
}
return null;
}
nc.f("fc", "input error: [" + s + "]");
return null;
}
j
함수는 위와 같다. PackageManager 를 통하여 앱의 버전 이름을 가져와서 반환하며 없으면 null 을 반환한다. ConfigData
의 a
, b
, c
함수가 무엇인지 확인해보겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ConfigData {
static {
System.loadLibrary("config");
}
public String a() {
return ConfigData.getRDataApk();
}
public String b() {
return ConfigData.getRDataApp();
}
public String c() {
return ConfigData.getRDataPath();
}
{ . . . }
}
config 라는 동적 라이브러리를 로드하여 ConfigData 의 메소드를 호출한다.
libconfig.so
파일을 열어보면 아래 함수콜을 찾아볼 수 있다.
1
2
3
4
jstring Java_Redacted_Redacted_Redacted_Redacted_ConfigData_getRDataApp(JNIEnv* param0) {
return param0[0]->NewStringUTF(param0, "com.tegrak.lagfix|eu.chainfire.supersu|com.noshufou.android.su|com.jrummy.root.browserfree|com.jrummy.busybox.installer|me.blog.markan.UnRooting|com.formyhm.hideroot");
}
j
힘수에서 봤듯이 메소드의 반환값을 |
을 기준으로 split 하는 것을 보았다.
이를 기반으로 위 메소드에서 문자열 배열로 나타내는 것은 아래와 같다.
1
2
3
4
5
6
7
"com.tegrak.lagfix"
"eu.chainfire.supersu"
"com.noshufou.android.su"
"com.jrummy.root.browserfree"
"com.jrummy.busybox.installer"
"me.blog.markan.UnRooting"
"com.formyhm.hideroot"
아래 함수 또한 j
힘수에서 봤듯이 메소드의 반환값을 |
을 기준으로 split 하는 것을 보았다.
1
2
3
4
jstring Java_Redacted_Redacted_Redacted_Redacted_ConfigData_getRDataPath(JNIEnv* param0) {
return param0[0]->NewStringUTF(param0, "/data/data/com.noshufou.android.su|/data/data/com.tegrak.lagfix|/data/data/eu.chainfire.supersu|/data/data/com.noshufou.android.su|/data/data/com.jrummy.root.browserfree");
}
위 메소드 또한 아래처럼 해석해줄 수 있다.
1
2
3
4
5
"/data/data/com.noshufou.android.su"
"/data/data/com.tegrak.lagfix"
"/data/data/eu.chainfire.supersu"
"/data/data/com.noshufou.android.su"
"/data/data/com.jrummy.root.browserfree"
아래 함수 또한 j
힘수에서 봤듯이 메소드의 반환값을 |
을 기준으로 split 하는 것을 보았다.
1
2
3
4
jstring Java_Redacted_Redacted_Redacted_Redacted_ConfigData_getRDataApk(JNIEnv* param0) {
return param0[0]->NewStringUTF(param0, "/su|/system/sbin|/su/xbin|/system/bin/su|/system/sbin7/su|/system/xbin/su|/system/bin/.user/.su|/dev/com.noshufou.android.su|/data/app/com.tegrak.lagfix.apk|/system/app/Superuser.apk|/data/app/eu.chainfire.supersu.apk|/data/app/com.noshufou.android.su.apk|/data/app/com.jrummy.root.browserfree.apk");
}
위 메소드 또한 아래처럼 해석해줄 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
"/su"
"/system/sbin"
"/su/xbin"
"/system/bin/su"
"/system/sbin7/su"
"/system/xbin/su"
"/system/bin/.user/.su"
"/dev/com.noshufou.android.su"
"/data/app/com.tegrak.lagfix.apk"
"/system/app/Superuser.apk"
"/data/app/eu.chainfire.supersu.apk"
"/data/app/com.noshufou.android.su.apk"
"/data/app/com.jrummy.root.browserfree.apk"
so 라이브러리를 열어보면 위와 같은 3개의 메소드를 확인할 수 있다.
이를 통해 j
함수를 다시 확인해보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public static String j(Context context0) {
boolean result;
ConfigData configData0 = new ConfigData();
configData0.a();
/*
"/su"
"/system/sbin"
"/su/xbin"
"/system/bin/su"
"/system/sbin7/su"
"/system/xbin/su"
"/system/bin/.user/.su"
"/dev/com.noshufou.android.su"
"/data/app/com.tegrak.lagfix.apk"
"/system/app/Superuser.apk"
"/data/app/eu.chainfire.supersu.apk"
"/data/app/com.noshufou.android.su.apk"
"/data/app/com.jrummy.root.browserfree.apk"
*/
String[] arr_s = configData0.a().split("\\|");
/*
"/data/data/com.noshufou.android.su"
"/data/data/com.tegrak.lagfix"
"/data/data/eu.chainfire.supersu"
"/data/data/com.noshufou.android.su"
"/data/data/com.jrummy.root.browserfree"
*/
String[] arr_s1 = configData0.c().split("\\|");
/*
"com.tegrak.lagfix"
"eu.chainfire.supersu"
"com.noshufou.android.su"
"com.jrummy.root.browserfree"
"com.jrummy.busybox.installer"
"me.blog.markan.UnRooting"
"com.formyhm.hideroot"
*/
String[] arr_s2 = configData0.b().split("\\|");
boolean res = false;
for(int v1 = 0; true; ++v1) {
result = true;
if(v1 >= arr_s.length) {
break;
}
if(new File(arr_s[v1]).exists()) { // 파일이 존재하는지
res = true;
}
if(res) {
break;
}
}
if(!res) {
for(int v2 = 0; v2 < arr_s1.length; ++v2) {
res = new File(arr_s1[v2]).isDirectory(); // 디렉터리가 존재하는지
if(res) {
break;
}
}
}
if(!res) {
for(int v = 0; v < arr_s2.length; ++v) {
String s = arr_s2[v];
if(fc.i(context0.getPackageManager(), s) != null) { // 해당 패키지 이름의 앱의 버전 이름 확인
return result ? "1" : "0";
}
}
}
result = res;
return result ? "1" : "0";
}
각각 특정 경로에 파일이 있는지 경로가 존재하는지 앱이 존재하는지로 진행하려고 한다.
이때 파일이나 디렉터리가 존재하지 않으면 탐지를 하지 못하는 경우가 있는데 이것은 신뢰하기 어려운 정보일 수 있다. 근래에는 일부 위협 기술에서 hide 기능이 강화되어 메모리 상에서 완전 관련 내용을 지우는 핵심 기능이 릴리즈되어 이러한 탐지 기능을 우회하는 양상을 보인다.
Conclusion
대부분 동적 분석으로 로직을 파악하려고 접근하지만 위와 같은 형식이거나 더 복잡한 RASP 으로 분석 방지 기능이 메모리 깊은 곳에 숨어있는 경우가 많다. 이를 우회하기 위해 Software-protection 을 무력화하거나 한 땀 한 땀 소스코드를 퍼즐 맞추기를 진행해야 할 수 있다. 근본적으로 정적 분석을 진행해야 할 수 있다. 이때 정적 분석 방지를 기능이 있어 파일 분석 또한 막혀있는 경우가 많다.
이렇다면 분석을 할 수 없는걸까?
이때는 파일의 무결성을 확인하는 동시에 보다 저수준으로 분석을 접근해야 한다. 이 방법은 이 장에서 자세히 설명하지 않겠다.
If you find any errors, please let me know by comment or email. Thank you.