# 문제 설명
- 안드로이드 문제를 많이 풀어보지 않았지만, 공부한것이 있어서 풀 수 있었습니다.
- MainActivity, Main2Activity, Main3Activity, Main4Activity이 주 클래스고 메인 클래스는 MainActivity입니다.
- 순서대로 ID, Password를 입력하면서 다음 액티비티로 넘어갑니다.
- ID는 제가 입력하는 값으로 아무 값으로 입력할 수 있으나, 이 값은 Main2Activity로 넘어갈 때 인텐트 값으로 제공되어 Password를 결정하게 됩니다.
- Main3Activity에서 시리얼키를 입력해야하는데, 시리얼키는 Random 클래스를 통해 생성되어 매번 Next 버튼을 눌러서, 클래스안의 함수를 호출할 떄마다 바뀌며 항상 바뀌는 값을 맞춰 주던가 다른 방법으로 우회해야 합니다.
- 마지막 Main3Activity에서 시리얼키 입력을 우회하여 Main4Activity 액티비티로 들어가게 되면, nativelib.so에서 만들어지는 FLAG가 EditText위에 적히게 됩니다.
# 환경 구성
- Windows 10
- Android Studio : IDE
- apk-signer-1.8.5 : 재서명을 할 수 있는 GUI 도구 입니다. 아주 편하게 쓰고 있습니다. 재서명을 했을 때 리소스 정렬도 해주어야 하는데 이 기능을 제공 해주는 편리한 도구 입니다.
- dex2jar : dex 추출기
- jd-gui : 디컴파일러
- dextools-softdx : 오덱스 과정을 편하게 할 수 있는 도구
# 문제 풀이
- 첫 시작 액티비티에서 "abcdefghijk"를 입력합니다. onClick 메소드 안의 조건문 식을 보면, 입력되는 값의 길이가 10이상 26이하 일때 다음 액티비티로 넘어가는 것을 확인할 수 있습니다. 그래서 11자리가 되는 "abcdefghijk"를 입력합니다.
- 아래 "hi there"이 아닌 Hello from C++은 nativelib에서 불러온 문자열입니다.
- 혹시나 MainActivity에서 먼저 NDK를 로딩해버리면 플래그가 뜨지 않을 까 해서 접근해본 방법이 였습니다.
- 첫 액티비티 입력 후 아래 소스에서 "abcdefghijk"라는 아이디에 대한 고정 PW를 만듭니다.
- 디컴파일러로 확인한 Java 코드를 이클립스 프로젝트로 생성 한 후 App에서 분리하여 PW 고정 값을 만들어 냅니다.
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 | package pro1; import java.util.Random; public class test1 { public static String encode(String arg10) { int v1 = 0; String v3 = ""; char[] v6 = new char[]{'c', 'o', 'd', 'e', 'g', 'a', 't', 'e', '2', '0', '1', '8', 'h', 'u', 'r', 'r', 'a', 'y', '!', 'H', 'A', 'H', 'A', 'H', 'A', 'L', 'O', 'L'}; int v7 = arg10.length(); int v4 = 0; String v5; for(v5 = ""; v4 < v7; v5 = v5 + String.valueOf(v0)) { int v2 = arg10.charAt(v4) ^ String.valueOf(v6).charAt(v7 + v4); int v0; for(v0 = 0; v0 < v6.length - 1; ++v0) { if(String.valueOf(v2).equals(Character.valueOf(String.valueOf(v6).charAt(v0)))) { v2 = new Random().nextInt(); } } for(v0 = 0; v0 < v2; v0 = v0 + v0 + 1) { } ++v4; } String v0_1 = v3; while(v1 < v7) { v0_1 = v0_1 + (v5.charAt(v1) ^ v1); ++v1; } return v0_1; } public static void main(String[] args) { String ret= ""; ret = test1.encode("abcdefghijk"); boolean check; check = ret.equals("4951535049545552575859"); # check : true , ret : 4951535049545552575859 System.out.println(check); System.out.println(ret); } } | cs |
- 제가 입력한 아이디로 생성되는 고정 PW는 "4951535049545552575859"입니다. 이 값을 Main2Activity에서 PW로 입력합니다.
- 다음 액티비티는 시리얼 키를 우회하는 부분 입니다. 코드 진행에 상관없이 제가 원하는 "crack"이라는 문자열로 리턴되도록 수정합니다.
- 디컴파일러로는 Java 코드를 수정할 수 없고 어셈블리어와 같이 JVM에서 저레벨 언어 역할을 하는 Smali Code를 수정하여 "crack"이라는 문자열을 넣어 줄 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public String k() { char[] v1 = new char[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; StringBuffer v2 = new StringBuffer(); Random v3 = new Random(); int v0; for(v0 = 0; v0 < 20; ++v0) { v2.append(v1[v3.nextInt(v1.length)]); } v2.toString(); return "crack"; } | cs |
- Main3Activity의 바이트코드 소스 입니다. 라인 4를 보시면 이 함수 안에서 사용하는 레지스터 갯수를 선언했고, 레지스터가 v0, v1, v2 ... 와 같은 형식으로 쓰이는 것을 보실 수 있습니다.
- 코드 해석을 전체로 본다면 조건 분기에 해당 합니다. 일반적인 어셈블리어 조건 분기와 같이 25번째 라인의 if-ge v0, v4 :cond_23 명령이 조건 분기를 하도록 합니다. v0이 v4보다 크다면(조건이 참이면) :cond_23으로 분기하라는 뜻 입니다. 아니라면 다음 라인을 실행합니다. 거짓이고 다음 라인을 실행하였고 명령어 포인터가 내려가다보면 :cond_23을 만나기 바로 이전에 goto :goto_12를 통해 해당 조건 블럭을 빠져나갑니다.
- 이 소스에서 한 명령어 단위를 해석하는 것은 제 경험적인 방법으로는 레지스터에 값이 입력되고(const vA, 등등) > 함수 호출(invoke) > 결과 반환(move-result) 으로 해석합니다.
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 | # virtual methods .method public k()Ljava/lang/String; .registers 6 const/16 v0, 0x24 new-array v1, v0, [C fill-array-data v1, :array_28 new-instance v2, Ljava/lang/StringBuffer; invoke-direct {v2}, Ljava/lang/StringBuffer;-><init>()V new-instance v3, Ljava/util/Random; invoke-direct {v3}, Ljava/util/Random;-><init>()V const/4 v0, 0x0 :goto_12 const/16 v4, 0x14 if-ge v0, v4, :cond_23 array-length v4, v1 invoke-virtual {v3, v4}, Ljava/util/Random;->nextInt(I)I move-result v4 aget-char v4, v1, v4 invoke-virtual {v2, v4}, Ljava/lang/StringBuffer;->append(C)Ljava/lang/StringBuffer; add-int/lit8 v0, v0, 0x1 goto :goto_12 :cond_23 invoke-virtual {v2}, Ljava/lang/StringBuffer;->toString()Ljava/lang/String; move-result-object v0 const-string v5, "crack" return-object v5 | cs |
- 원래 는 42번째 라인과 44번째 라인이 하나의 명령어로 실행 되어 return-object 명령에 v0가 리턴되지만 이전 명령이 끝나는 부분 바로 아래에 제가 리턴하고자 하는 문자열 "crack"을 만들고 그 값을 담고 있는 레지스터를 리턴해 주 었습니다.
- 이제 다시 smali 소스를 오덱싱하여 apk로 만들고 실행하여 Main3Activity에서 crack을 입력하면 플래그가 뜹니다.
# Flag
- FLAG {W3_w3r3_Back_70_$n7een!!!}
# 오류
- 에뮬레이터 x86_64와 x86에서 돌아가는 것에 차이가 있었습니다. 에뮬레이터를 많이 사용해봤던터라 x86에뮬레이터보다는 x86_64가 체감상 빠르다는걸 느끼고 x86_64로 진행 했었는데, x86_64환경 에서 진행하면 마지막 Main3Activity를 우회하고 Main4Activity가 시작될 때 NDK 안의 stringFromJNI()함수를 찾지 못합니다. .so 파일을 보면 실제로 찾는 메소드 경로는 java.lang.String com.example.puing.a2018codegate.Main4Activity.stringFromJNI() 이지만, 실제로 저장되는 메소드 이름은(NDK에서는 java의 패키지경로_메소드로 함수이름을 정합니다.) java.lang.String com.example.puing.a2018codegate.MainActivity.stringFromJNI()이여서 찾지 못합니다. x86 에뮬레이터에서는 정상적으로 찾아지는 경로였고, 문제가 만들어질 당시 수정되지않은 문제 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 02-03 07:33:25.813 12383-12383/? E/art: No implementation found for java.lang.String com.example.puing.a2018codegate.Main4Activity.stringFromJNI() (tried Java_com_example_puing_a2018codegate_Main4Activity_stringFromJNI and Java_com_example_puing_a2018codegate_Main4Activity_stringFromJNI__) 02-03 07:33:25.813 12383-12383/? E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.puing.a2018codegate, PID: 12383 java.lang.UnsatisfiedLinkError: No implementation found for java.lang.String com.example.puing.a2018codegate.Main4Activity.stringFromJNI() (tried Java_com_example_puing_a2018codegate_Main4Activity_stringFromJNI and Java_com_example_puing_a2018codegate_Main4Activity_stringFromJNI__) at com.example.puing.a2018codegate.Main4Activity.stringFromJNI(Native Method) at com.example.puing.a2018codegate.Main4Activity.onCreate(Unknown Source) at android.app.Activity.performCreate(Activity.java:6237) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2369) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476) at android.app.ActivityThread.-wrap11(ActivityThread.java) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:148) at android.app.ActivityThread.main(ActivityThread.java:5417) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) | cs |