상세 컨텐츠

본문 제목

[2020 Samsung CTF] Vault 101 (Android Reversing)

REVERSING/Android

by koharin 2021. 8. 2. 20:31

본문

728x90
반응형

Setting


Environment

  • Emulator: Android Studio

Download

FrigidSec/CTFWriteups

 

 

Decompile


apktool

apktool d Vault101-1.1-release.apk

 

dex2jar

d2j-dex2jar.bat -f Vault101-1.1-release.apk

Java 코드 분석 위해 classes.dex → jar로 변환

apk 파일을 unzip해서 얻은 classes.dex 파일을 대상으로 dex2jar 디컴파일러로 jar을 얻을 수도 있다.

 

JD-GUI, jadx

JD-GUI 또는 jadx 툴을 사용하여 Java 코드를 확인했다.

분석하던 중 jd-gui로 제대로 디컴파일이 안 된 코드들이 있어서 jadx를 병행해서 사용했다.

 

 

Dynamic Analysis


처음 실행 시, Flag를 입력하는 EditText 필드가 있다.

 

 

 

Code Analysis


 

MainActivity.xml

<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="29" android:compileSdkVersionCodename="10" package="com.sctf2020.vault101" platformBuildVersionCode="29" platformBuildVersionName="10">
    <application android:allowBackup="false" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:extractNativeLibs="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:name="com.sctf2020.vault101.VaultApplication" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme">
        <service android:enabled="true" android:exported="true" android:name="com.sctf2020.vault101.VaultService"/>
        <activity android:name="com.sctf2020.vault101.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

 

MainActivity.class: onClick

public void onClick(View view) {
        try {
            boolean a2 = this.s.a(this.p.getText().toString());
            Toast toast = new Toast(this);
            toast.setView(getLayoutInflater().inflate(a2 ? R.layout.toast_success_layout : R.layout.toast_fail_layout, (ViewGroup) findViewById(R.id.custom_toast_container)));
            toast.setGravity(17, 0, 0);
            toast.setDuration(1);
            toast.show();
            if (!a2) {
                this.p.getText().clear();
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
}
  • Verify를 텍스트로 가지는 Button에 Click Event가 발생하면, onClick에 정의된 동작을 수행한다.
  • boolean a2 = this.s.a(this.p.getText().toString()); EditText 변수인 p에서 텍스트를 스트링으로 변환한 것을 this.s.a 함수 인자로 전달하여 boolean 값을 반환받는다. flag와 일치 여부에 대한 boolean 값임을 짐작할 수 있다.
  • toast.setView(getLayoutInflater().inflate(a2 ? R.layout.toast_success_layout : R.layout.toast_fail_layout, (ViewGroup) findViewById(R.id.custom_toast_container))); 이후 a2 값에 따라 True일 경우 success를 toast 메시지로 보여주고, False일 경우 Fail을 toast 메시지로 보여주는 것을 알 수 있다. 그리고 a2 값이 False인 경우에는 EditText 필드 텍스트를 초기화한다.
  • 즉, a2 값이 True가 되도록 만들어서 success를 toast 메시지로 보여주도록 해야 한다.

this.s.a 함수 코드에 접근할 방법을 찾아보자.

public class MainActivity extends e implements View.OnClickListener {
    public EditText p;
    public Button q;
    public AnimatedVectorDrawable r;
    public volatile b.c.a.b s;
    public ServiceConnection t = new a();
  • s는 b.c.a.b 임을 알 수 있다. 따라서 s.a는 boolean b.c.a.b.a(String str) 함수이다.

 

public static class a implements b {
      public IBinder a;
      
      public a(IBinder param2IBinder) {
        this.a = param2IBinder;
      }
      
      public boolean a(String param2String) {
        Parcel parcel1 = Parcel.obtain();
        Parcel parcel2 = Parcel.obtain();
        try {
          parcel1.writeInterfaceToken("com.sctf2020.vault101.IVault");
          parcel1.writeString(param2String);
          IBinder iBinder = this.a;
          boolean bool = false;
          iBinder.transact(1, parcel1, parcel2, 0);
          parcel2.readException();
          int i = parcel2.readInt();
          if (i != 0)
            bool = true; 
          return bool;
        } finally {
          parcel2.recycle();
          parcel1.recycle();
        } 
      }
      
      public IBinder asBinder() {
        return this.a;
      }
    }
  }
  
  public static class a implements b {
    public IBinder a;
    
    public a(IBinder param1IBinder) {
      this.a = param1IBinder;
    }
    
    public boolean a(String param1String) {
      Parcel parcel1 = Parcel.obtain();
      Parcel parcel2 = Parcel.obtain();
      try {
        parcel1.writeInterfaceToken("com.sctf2020.vault101.IVault");
        parcel1.writeString(param1String);
        IBinder iBinder = this.a;
        boolean bool = false;
        iBinder.transact(1, parcel1, parcel2, 0);
        parcel2.readException();
        int i = parcel2.readInt();
        if (i != 0)
          bool = true; 
        return bool;
      } finally {
        parcel2.recycle();
        parcel1.recycle();
      } 
    }
    
    public IBinder asBinder() {
      return this.a;
    }
  }
  • b.c.a.b 클래스에서 리턴형이 boolean이고 매개변수로 String 타입 1개를 가지는 함수 a를 찾을 수 있다.
  • Parcel 클래스: 안드로이드에서 IPC 전용 데이터로 사용하기 위해 만든 클래스. 프로세스 간 데이터 전달에 최적화되어 있어 속도가 빠르다.
  • Parcel.obtain() : Parcel 객체 생성하고, Primitive Type 데이터를 사용함
  • android 공식문서에 따르면, IBinder API인 transact()는 Binder.onTransact()에 대응한다. 따라서 a 함수에서의 transact() 동작 과정은 onTransact()를 보면 된다.

 

public boolean onTransact(int param1Int1, Parcel param1Parcel1, Parcel param1Parcel2, int param1Int2) {
      if (param1Int1 != 1) {
        if (param1Int1 != 1598968902) // 1598968902 == -1
          return super.onTransact(param1Int1, param1Parcel1, param1Parcel2, param1Int2); 
        param1Parcel2.writeString("com.sctf2020.vault101.IVault");
        return true;
      } 
      param1Parcel1.enforceInterface("com.sctf2020.vault101.IVault");
      boolean bool = a(param1Parcel1.readString());
      param1Parcel2.writeNoException();
      param1Parcel2.writeInt(bool);
      return true;
    }
  • param1Int1 값이 1이면, boolean bool = a(param1Parcel1.readString()); 으로 a 메소드를 호출한다.
  • 이때 a 메소드는 b.c.a.b.a를 상속받은 VaultService 클래스 내 b 클래스의 a 메소드이다.

 

// com.sctf2020.vault101.VaultService
@Override // b.c.a.b
        public boolean a(String str) {
            try {
                int i = this.f874a + 1;
                this.f874a = i;
                if (i > 3) {
                    Class.forName(c.d(";È\u0003p¯…4ŶorÂ\"Ý\u0010|", -500953648)).getMethod(c.d("qó%", 991422357), (Class) Class.forName(c.d("~jxe\u0005reíY:Bè`niaY", 1069257791)).getDeclaredField(c.d("\u0001ò¬\u0010", 1659367412)).get(null)).invoke(null, 0);
                    return false;
                } else if (str == null) {
                    return false;
                } else {
                    byte[] b2 = b.c.a.a.b((byte[]) Class.forName(c.d(".®$\u000fß1Ç\u0003?ڙ6ʶ\"", 1451800421)).getMethod(c.d("7Ì£\u0002rØ0X", -552283301), new Class[0]).invoke(str, new Object[0]));
                    Object invoke = Class.forName(c.d("aogrfle¯}qjì.Cbsl35", 823239689)).getMethod(c.d("$OX{Í\u0010", -2050089752), Class.forName(c.d("\u000eé", 937562454)), (Class) Class.forName(c.d(";HCp¯\u0005tå¶ohõ%LRtó", -730536752)).getDeclaredField(c.d("\u0014ø¡\u0014", -1215097919)).get(null)).invoke(null, b2, Class.forName(c.d("pç\u000bfÆ´!\rÌ!BZ?Ë\u000egÌëq", -1393972808)).getDeclaredField(c.d("\u001aä‰\u0017Yï\u0004", 1778992991)).get(null));
                    Object newInstance = Class.forName(c.d("~jxe\u0005remY:Xrfb`c", 1356052543)).getConstructor(Class.forName(c.d("[C", 591904395))).newInstance(invoke);
                    Object invoke2 = Class.forName(c.d("$Í\u001efƞ4\u0007Ž:EH Í\u000e:ê˜>]ˆ-_", -248372756)).getMethod(c.d("&DÇ\u0003ÿ\u0016xg¬", 64103114), (Class) Class.forName(c.d("/BNt\u001fqç‚`ó1F_pÓ", -401453852)).getDeclaredField(c.d("\u0001ò\u0004D", 195131734)).get(null)).invoke(VaultService.this, Integer.valueOf((int) R.string.magic));
                    return ((Boolean) Class.forName(c.d("zhs`-ddmu.Rwb`kf", -754317293)).getMethod(c.d("tø\u001auÅ®", 528601528), Class.forName(c.d("oâœ5…º!OÈzä\u0016oæ‰ ", -1620091986))).invoke(invoke2, newInstance)).booleanValue();
                }
            } catch (Throwable unused) {
                throw new RuntimeException();
            }
}
  • VaultService에는 b.c.a.b.a 메소드를 override한 a 메소드가 있다.
  • 해당 메소드에서는 b.c.a.a.b 메소드를 호출한다.

 

package b.c.a;

public class a {

    /* renamed from: a  reason: collision with root package name */
    public static byte[] f821a;

    public static byte[] a(byte[] bArr) {
        try {
            Object invoke = Class.forName(c.d(">JÂ0ûDwù¯0Õ´zhÝ!ë\u000ff", -989022505)).getMethod(c.d("rî:MGì0BSvn", -2055632580), Class.forName(c.d("+À‡0­Œ0G¤nò\r3ȟ6", 675347394))).invoke(null, c.d("QLÜkh^F,j_È\u0015%Yî Oukd", -1263812037));
            Object newInstance = Class.forName(c.d("zè3`y§&QiquL>ú5db§\u0016FssdW[ì<Rqì&", -451507951)).getConstructor(Class.forName(c.d("KË", -867135215)), Class.forName(c.d(":ȁ1¥€4ŽjðŽ\"À™7", 336766939))).newInstance(a.class.getDeclaredFields()[0].get(null), c.d("\u0005fã", -930415931));
            Object newInstance2 = Class.forName(c.d("obVpûlcsþ0U~+pPtàlIw×!SphfTtñ\u0011pdä", -768401274)).getConstructor(Class.forName(c.d("\u000eé", 194738646))).newInstance(a.class.getDeclaredFields()[0].get(null));
            Object obj = Class.forName(c.d("*À§ {Ž2[;põ\u0006nâ¸1kÅ#", -822830269)).getDeclaredField(c.d("\u0014lþ\u0007ú(AT¿\u001fí\u001a", 1548356339)).get(null);
            Class.forName(c.d(";È!aqƒ6Ù uwÅê>paÈ'", 370203992)).getMethod(c.d("lmiu", 1209022468), (Class) Class.forName(c.d(";È)e\u0005Ð4Å\u001c:BÀ%Ì8aY", -341371526)).getDeclaredField(c.d("AR|P", 1490185396)).get(null), Class.forName(c.d("kà\u0017p¯²$Bô3HEx¯*tø", -1361421952)), Class.forName(c.d("nbÞ4 ebÑ#À!}-Û%Ä0.@È6Æ'mwÀ8ñ2r`É4Ý0vPØ0Â", -702764379))).invoke(invoke, obj, newInstance, newInstance2);
            return (byte[]) Class.forName(c.d("jú$Y5gqLaý(;Hå5I~v", 1547207220)).getMethod(c.d("0Dpyå\u000fx", 148803807), Class.forName(c.d("NI", -1487124972))).invoke(invoke, bArr);
        } catch (Throwable unused) {
            throw new RuntimeException();
        }
    }

    public static byte[] b(byte[] bArr) {
        try {
            Object invoke = Class.forName(c.d(".Â$aq‰3[5uwGjà;paÂ\"", 427878733)).getMethod(c.d("wlû\rEoqbTwî", 498197051), Class.forName(c.d("+@g`/Mpg&/R}3Hf", -2117115840))).invoke(null, c.d("\u0004fa?Ê%S&\u000e`KpsStí\u000e~n", -497723188));
            Object newInstance = Class.forName(c.d("+À%asŠ2Û2twÇoÒ#ehŠ\u0002Ì(vfÜ\nÄ*S{Á2", 1399586122)).getConstructor(Class.forName(c.d("NI", -1666818412)), Class.forName(c.d("?Ê\nt—5E’zK'Â\u0012r", 1345337844))).newInstance(a.class.getDeclaredFields()[0].get(null), c.d("\u0000ä\"", 1922633154));
            Object newInstance2 = Class.forName(c.d("{hñ!s\"fqbt÷-?z÷%h\"LuKeñ#|ló%y_ufx", -66160102)).getConstructor(Class.forName(c.d("\u001aã", -1333368352))).newInstance(a.class.getDeclaredFields()[0].get(null));
            Object obj = Class.forName(c.d("?JÊ4ÛTwyŽ Ý0{hÕ%Ë\u001ff", -1437859082)).getDeclaredField(c.d("\u0014ç\u001cVpí\u0001t4ZOk", -990494344)).get(null);
            Class.forName(c.d("~ê0aq¡'QeuwM:È/paê6", 1004120349)).getMethod(c.d("mmiu", 1589187589), (Class) Class.forName(c.d(":HÉ5‡\u0011teŸ{â0$LØ1Û", -1773462791)).getDeclaredField(c.d("@Җ\u0005", 1348396831)).get(null), Class.forName(c.d("j`Uq¥7dâÿ6Jäy/huò", -1730593653)), Class.forName(c.d("nbta'teâywjô}-qpld.À`blòmwjmYfràa`wåvPrej", -779848691))).invoke(invoke, obj, newInstance, newInstance2);
            return (byte[]) Class.forName(c.d(">Jê$[\u0014wy\u000f`ý zhõ5K_f", 813386359)).getMethod(c.d("qä‚(më(", -186753258), Class.forName(c.d("_Á", -251609689))).invoke(invoke, bArr);
        } catch (Throwable unused) {
            throw new RuntimeException();
        }
    }
}

하지만 두 메소드 모두 난독화되어 스트링을 확인할 수 없다.

c.d() 메소드에 어떤 스트링과 숫자를 인자로 전달하고 있는 것만 알 수 있다.

c.d() 메소드를 찾아보자.

 

package b.c.a;

public class c {

    /* renamed from: a  reason: collision with root package name */
    public static int f823a;

    public static char a(char c, int i) {
        return (char) (c & ((1 << i) ^ 65535));
    }

    public static char b(char c, int i) {
        return (char) (c | (1 << i));
    }

    public static char c(char c, int i) {
        return (char) ((c & (1 << i)) >> i);
    }

    public static String d(CharSequence charSequence, int i) {
        StringBuilder sb = new StringBuilder();
        if (i == 0) {
            return sb.toString();
        }
        for (int i2 = 0; i2 < charSequence.length(); i2++) {
            char charAt = charSequence.charAt(i2);
            char c = (char) (i >> (i2 % 4));
            int i3 = i2 % 3;
            if (i3 == 0) {
                for (int i4 = 0; i4 < 8; i4 += 2) {
                    int c2 = c(charAt, i4) ^ c(c, i4);
                    if (c2 == 0) {
                        charAt = a(charAt, i4);
                    } else if (c2 == 1) {
                        charAt = b(charAt, i4);
                    }
                }
            } else if (i3 == 1) {
                for (int i5 = 1; i5 < 8; i5 += 2) {
                    int c3 = c(charAt, i5) ^ c(c, i5);
                    if (c3 == 0) {
                        charAt = a(charAt, i5);
                    } else if (c3 == 1) {
                        charAt = b(charAt, i5);
                    }
                }
            } else if (i3 == 2) {
                for (int i6 = 0; i6 < 8; i6++) {
                    int c4 = c(charAt, i6) ^ c(c, i6);
                    if (c4 == 0) {
                        charAt = a(charAt, i6);
                    } else if (c4 == 1) {
                        charAt = b(charAt, i6);
                    }
                }
            }
            sb.append((char) (charAt ^ f823a));
        }
        return sb.toString();
    }
}
  • 스트링을 난독화를 어떠한 로직으로 풀고 해당 sb 스트링을 반환하는 것을 알 수 있다. 따라서 해당 코드를 그대로 사용하여 스트링들을 deobfuscate해보자.

 

Deobfuscation

package com.sctf2020.vault101;

import android.app.Application;
import b.c.a.c;

public class VaultApplication extends Application {
    public void onCreate() {
        super.onCreate();
        c.f823a = 1;
    }
}
  • VaultApplication에서 c.f823a = 1로 설정되어 있다.
  • 따라서 public static int f823a; 에서 f823a가 1인 것으로 간주하고 deobfuscate를 진행하면 된다.

 

난독화 해제 코드 

public class vault_101 {
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.println(d(";È\u0003p¯…4ŶorÂ\"Ý\u0010|", -500953648));
		System.out.println(d("qó%", 991422357));
		System.out.println(d("~jxe\u0005reíY:Bè`niaY", 1069257791));
		System.out.println(d("\u0001ò¬\u0010", 1659367412));
		System.out.println(d(".®$\u000fß1Ç\u0003?ڙ6ʶ\"", 1451800421));
		System.out.println(d("7Ì£\u0002rØ0X", -552283301));
		System.out.println(d("aogrfle¯}qjì.Cbsl35", 823239689));
		System.out.println(d("$OX{Í\u0010", -2050089752));
		System.out.println(d("\u000eé", 937562454));
		System.out.println(d(";HCp¯\u0005tå¶ohõ%LRtó", -730536752));
		System.out.println(d("\u0014ø¡\u0014", -1215097919));
		System.out.println(d("pç\u000bfÆ´!\rÌ!BZ?Ë\u000egÌëq", -1393972808));
		System.out.println(d("\u001aä‰\u0017Yï\u0004", 1778992991));
		System.out.println(d("~jxe\u0005remY:Xrfb`c", 1356052543));
		System.out.println(d("[C", 591904395));
		System.out.println(d("$Í\u001efƞ4\u0007Ž:EH Í\u000e:ê˜>]ˆ-_", -248372756));
		System.out.println(d("&DÇ\u0003ÿ\u0016xg¬", 64103114));
		System.out.println(d("/BNt\u001fqç‚`ó1F_pÓ", -401453852));
		System.out.println(d("\u0001ò\u0004D", 195131734));
		System.out.println(d("zhs`-ddmu.Rwb`kf", -754317293));
		System.out.println(d("tø\u001auÅ®", 528601528));
		System.out.println(d("oâœ5…º!OÈzä\u0016oæ‰ ", -1620091986));
		System.out.println(d("!ϕ#îˆ5‡£.ύ%υó„\"‡’$Җ5Ӓ4ò", 354045889));
		System.out.println(d("#Fä\u0012uQyç#@ó»%Z", 1074255429));
		System.out.println(d("ê1§£%Íúkêœaî5û", -72994916));
		System.out.println(d("\u0010zBE", 1821340751));
		System.out.println(d("0Gqsn@q¥2noÿ4Ga/BF{ÿ4yu", 1132272720));
		System.out.println(d("bf~VNeoô]wn÷", 1545475118));
		System.out.println(d("k`Õ1¥(`oìjð$shÍ7", 25375370));
		System.out.println(d("wã\u000ffè«", -375635523));
		System.out.println(d("oâ6`/ï!Ob/HOqæ'ds", 1759650052));
		System.out.println(d("\u0000rÌ\u0000", 575374967));
		System.out.println(d(";HÁ1§\u0001te¾kð.#@Ù7", -158782760));
		//System.out.println(d((CharSequence) d(";HÁ1§\u0001te¾kð.#@Ù7", -158782760)));
		System.out.println(d("JË", 543344920));
		System.out.println(d("uåŠ&Ä· \rË Â\u001a:ɏ'Îèp", 446383039));
		System.out.println(d(" Fñ/mB", 825348685));
		System.out.println(d("+À§ -Ì0Ç$.ҝ3È¿&", 1167863618));
		System.out.println(d("?ʨ%\u0007Ó5E\u001a;Â\u0000!ι![", -521211012));
		System.out.println(d("\u0010zè\u0010", -1491966233));
		System.out.println(d("$M^fÆ\u001et§˜!Bðka[gÌA$", -1048288020));
		System.out.println(d("\nlÇ\u0012qs@", 282303079));
	}
	public static int INT=1;

	public static char a(char c, int i) {
        return (char) (c & ((1 << i) ^ 65535));
    }

    public static char b(char c, int i) {
        return (char) (c | (1 << i));
    }

    public static char c(char c, int i) {
        return (char) ((c & (1 << i)) >> i);
    }

	public static String d(CharSequence charSequence, int i) {
        StringBuilder sb = new StringBuilder();
        if (i == 0) {
            return sb.toString();
        }
        for (int i2 = 0; i2 < charSequence.length(); i2++) {
            char charAt = charSequence.charAt(i2);
            char c = (char) (i >> (i2 % 4));
            int i3 = i2 % 3;
            if (i3 == 0) {
                for (int i4 = 0; i4 < 8; i4 += 2) {
                    int c2 = c(charAt, i4) ^ c(c, i4);
                    if (c2 == 0) {
                        charAt = a(charAt, i4);
                    } else if (c2 == 1) {
                        charAt = b(charAt, i4);
                    }
                }
            } else if (i3 == 1) {
                for (int i5 = 1; i5 < 8; i5 += 2) {
                    int c3 = c(charAt, i5) ^ c(c, i5);
                    if (c3 == 0) {
                        charAt = a(charAt, i5);
                    } else if (c3 == 1) {
                        charAt = b(charAt, i5);
                    }
                }
            } else if (i3 == 2) {
                for (int i6 = 0; i6 < 8; i6++) {
                    int c4 = c(charAt, i6) ^ c(c, i6);
                    if (c4 == 0) {
                        charAt = a(charAt, i6);
                    } else if (c4 == 1) {
                        charAt = b(charAt, i6);
                    }
                }
            }
            sb.append((char) (charAt ^ INT));
        }
        return sb.toString();
    }
}

c.d() 메소드의 난독화 해제 코드를 이용하여 각 스트링을 사람이 읽을 수 있는 스트링으로 변환하는 코드를 구현했다.

위의 Java 코드를 실행하면 다음의 결과를 얻을 수 있다.

# OUTPUT
java.lang.System
exit
java.lang.Integer
TYPE
java.lang.String
getBytes
android.util.Base64
encode
[B
java.lang.Integer
TYPE
android.util.Base64
NO_WRAP
java.lang.String
[B
android.content.Context
getString
java.lang.Integer
TYPE
java.lang.String
equals
java.lang.Object
android.content.res.Resources
getStringArray
java.lang.Integer
TYPE
android.content.Context
getResources
java.lang.String
charAt
java.lang.Integer
TYPE
java.lang.String
[B
android.util.Base64
decode
java.lang.String
java.lang.Integer
TYPE
android.util.Base64
NO_WRAP
package com.sctf2020.vault101;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import b.c.a.b;
import b.c.a.c;
import java.lang.reflect.Array;

public class VaultService extends Service {

    /* renamed from: b  reason: collision with root package name */
    public IBinder f873b = new b(null);

    public class b extends b.a {

        /* renamed from: a  reason: collision with root package name */
        public int f874a;

        public b(a aVar) {
        }

        @Override // b.c.a.b
        public boolean a(String str) {
            try {
                int i = this.f874a + 1;
                this.f874a = i;
                if (i > 3) {
                    Class.forName(java.lang.System).getMethod(exit, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(null, 0);
                    return false;
                } else if (str == null) {
                    return false;
                } else {
                    byte[] b2 = b.c.a.a.b((byte[]) Class.forName(java.lang.String).getMethod(getBytes, new Class[0]).invoke(str, new Object[0]));
                    Object invoke = Class.forName(android.util.Base64).getMethod(encode, Class.forName([B), (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(null, b2, Class.forName(android.util.Base64).getDeclaredField(NO_WRAP).get(null));
                    Object newInstance = Class.forName(java.lang.String).getConstructor(Class.forName([B)).newInstance(invoke);
                    Object invoke2 = Class.forName(android.content.Context).getMethod(getString, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(VaultService.this, Integer.valueOf((int) R.string.magic));
                    return ((Boolean) Class.forName(java.lang.String).getMethod(equals, Class.forName(java.lang.Object)).invoke(invoke2, newInstance)).booleanValue();
                }
            } catch (Throwable unused) {
                throw new RuntimeException();
            }
        }
    }

    public IBinder onBind(Intent intent) {
        return this.f873b;
    }

    public void onCreate() {
        try {
            Object invoke = Class.forName(android.content.res.Resources).getMethod(getStringArray, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(Class.forName(android.content.Context).getMethod(getResources, new Class[0]).invoke(this, new Object[0]), Integer.valueOf((int) R.array.kind_of_magic));
            if (invoke != null) {
                int length = Array.getLength(invoke);
                byte[] bArr = new byte[length];
                for (int i = 0; i < length; i++) {
                    bArr[i] = (byte) ((Character) Class.forName(java.lang.String).getMethod(charAt, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(c.d((CharSequence) Class.forName(java.lang.String).getConstructor(Class.forName([B)).newInstance(Class.forName(android.util.Base64).getMethod(decode, Class.forName(java.lang.String), (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(null, Array.get(invoke, i), Class.forName(android.util.Base64).getDeclaredField(NO_WRAP).get(null))), i ^ 137), 0)).charValue();
                }
                b.c.a.a.class.getDeclaredFields()[0].set(null, bArr);
                return;
            }
            throw new NullPointerException();
        } catch (Throwable unused) {
            throw new RuntimeException();
        }
    }
}

VaultService 코드의 스트링을 구한 스트링으로 대체하면 위와 같다.

 

javax.crypto.Cipher
getInstance
java.lang.String
AES/CBC/PKCS5Padding
javax.crypto.spec.SecretKeySpec
[B
java.lang.String
AES
javax.crypto.spec.IvParameterSpec
[B
javax.crypto.Cipher
DECRYPT_MODE
javax.crypto.Cipher
init
java.lang.Integer
TYPE
java.security.Key
java.security.spec.AlgorithmParameterSpec
javax.crypto.Cipher
doFinal
[B
javax.crypto.Cipher
getInstance
java.lang.String
AES/CBC/PKCS5Padding
javax.crypto.spec.SecretKeySpec
[B
java.lang.String
AES
javax.crypto.spec.IvParameterSpec
[B
javax.crypto.Cipher
ENCRYPT_MODE
javax.crypto.Cipher
init
java.lang.Integer
TYPE
java.security.Key
java.security.spec.AlgorithmParameterSpec
javax.crypto.Cipher
doFinal
[B
----------------------------------------------------------------
javax.crypto.Cipher
getInstance
java.lang.String
AES/CBC/PKCS5Padding
javax.crypto.spec.SecretKeySpec
[B
java.lang.String
AES
javax.crypto.spec.IvParameterSpec
[B
javax.crypto.Cipher
ENCRYPT_MODE
javax.crypto.Cipher
init
java.lang.Integer
TYPE
java.security.Key
java.security.spec.AlgorithmParameterSpec
javax.crypto.Cipher
doFinal
[B
package b.c.a;

public class a {

    /* renamed from: a  reason: collision with root package name */
    public static byte[] f821a;

    public static byte[] a(byte[] bArr) {
        try {
            Object invoke = Class.forName(javax.crypto.Cipher).getMethod(getInstance, Class.forName(java.lang.String)).invoke(null, AES/CBC/PKCS5Padding);
            Object newInstance = Class.forName(javax.crypto.spec.SecretKeySpec).getConstructor(Class.forName(c.d("KË", -867135215)), Class.forName(java.lang.String)).newInstance(a.class.getDeclaredFields()[0].get(null), AES);
            Object newInstance2 = Class.forName(javax.crypto.spec.IvParameterSpec).getConstructor(Class.forName([B)).newInstance(a.class.getDeclaredFields()[0].get(null));
            Object obj = Class.forName(javax.crypto.Cipher).getDeclaredField(DECRYPT_MODE).get(null);
            Class.forName(javax.crypto.Cipher).getMethod(init, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null), Class.forName(java.security.Key), Class.forName(java.security.spec.AlgorithmParameterSpec)).invoke(invoke, obj, newInstance, newInstance2);
            return (byte[]) Class.forName(javax.crypto.Cipher).getMethod(doFinal, Class.forName(c.d("NI", -1487124972)).invoke(invoke, bArr);
        } catch (Throwable unused) {
            throw new RuntimeException();
        }
    }

   public static byte[] b(byte[] bArr) {
        try {
            Object invoke = Class.forName(javax.crypto.Cipher).getMethod(getInstance, Class.forName(java.lang.String)).invoke(null, AES/CBC/PKCS5Padding);
            Object newInstance = Class.forName(javax.crypto.spec.SecretKeySpec).getConstructor(Class.forName("[B"), Class.forName(java.lang.String)).newInstance(a.class.getDeclaredFields()[0].get(null), AES);
            Object newInstance2 = Class.forName(javax.crypto.spec.IvParameterSpec).getConstructor(Class.forName("[B")).newInstance(a.class.getDeclaredFields()[0].get(null));
            Object obj = Class.forName(javax.crypto.Cipher).getDeclaredField(ENCRYPT_MODE).get(null);
            Class.forName(javax.crypto.Cipher).getMethod(init, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null), Class.forName(java.security.Key), Class.forName(java.security.spec.AlgorithmParameterSpec)).invoke(invoke, obj, newInstance, newInstance2);
            return (byte[]) Class.forName(javax.crypto.Cipher).getMethod(doFinal, Class.forName("[B")).invoke(invoke, bArr);
        } catch (Throwable unused) {
            throw new RuntimeException();
        }
    }
}

b.c.a.a 클래스는 위와 같다.

위의 코드는 다음과 같이 해석하면 된다.

Class.forName(javax.crypto.Cipher).getMethod(getInstance).invoke("AES/CBC/PKCS5Padding"); 는 다음과 동일하다.

Cipher.getInstance("AES/CBC/PKCS5Padding");

 

 

Execution Sequence


1. MainActivity.java

public void onClick(View view) {
        try {
            boolean a2 = this.s.a(this.p.getText().toString());
            Toast toast = new Toast(this);
            toast.setView(getLayoutInflater().inflate(a2 ? R.layout.toast_success_layout : R.layout.toast_fail_layout, (ViewGroup) findViewById(R.id.custom_toast_container)));
            toast.setGravity(17, 0, 0);
            toast.setDuration(1);
            toast.show();
            if (!a2) {
                this.p.getText().clear();
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
@Override // a.j.a.e, androidx.activity.ComponentActivity, a.g.c.d, a.b.k.e
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.activity_main);
        this.q = (Button) findViewById(R.id.button);
        EditText editText = (EditText) findViewById(R.id.editText);
        this.p = editText;
        editText.addTextChangedListener(new b());
        AnimatedVectorDrawable animatedVectorDrawable = (AnimatedVectorDrawable) ((ImageView) findViewById(R.id.imageView)).getDrawable();
        this.r = animatedVectorDrawable;
        animatedVectorDrawable.start();
        bindService(new Intent(this, VaultService.class), this.t, 1);
    }
  • EditText에 스트링을 입력받는다.
  • 입력받은 스트링을 VaultService로 넘긴다.

 

2. VaultService.class

@Override // b.c.a.b
        public boolean a(String str) {
            try {
                int i = this.f874a + 1;
                this.f874a = i;
                if (i > 3) {
                    Class.forName(java.lang.System).getMethod(exit, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(null, 0);
                    return false;
                } else if (str == null) {
                    return false;
                } else {
                    byte[] b2 = b.c.a.a.b((byte[]) Class.forName(java.lang.String).getMethod(getBytes, new Class[0]).invoke(str, new Object[0]));
                    Object invoke = Class.forName(android.util.Base64).getMethod(encode, Class.forName("[B"), (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(null, b2, Class.forName(android.util.Base64).getDeclaredField(NO_WRAP).get(null));
                    Object newInstance = Class.forName(java.lang.String).getConstructor(Class.forName("[B")).newInstance(invoke);
                    Object invoke2 = Class.forName(android.content.Context).getMethod(getString, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(VaultService.this, Integer.valueOf((int) R.string.magic));
                    return ((Boolean) Class.forName(java.lang.String).getMethod(equals, Class.forName(java.lang.Object)).invoke(invoke2, newInstance)).booleanValue();
                }
            } catch (Throwable unused) {
                throw new RuntimeException();
            }
        }

입력받은 스트링은 b.c.a.a. 클래스의 b 메소드 인자로 넘긴다. encrypt하기 위해서이다.

b.c.a.a 클래스의 b 메소드는 AES 암호화 알고리즘을 base64로 하드코딩된 key와 IV를 사용하여 스트링을 암호화한다.

public void onCreate() {
        try {
            Object invoke = Class.forName(android.content.res.Resources).getMethod(getStringArray, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(Class.forName(android.content.Context).getMethod(getResources, new Class[0]).invoke(this, new Object[0]), Integer.valueOf((int) R.array.kind_of_magic));
            if (invoke != null) {
                int length = Array.getLength(invoke);
                byte[] bArr = new byte[length];
                for (int i = 0; i < length; i++) {
                    bArr[i] = (byte) ((Character) Class.forName(java.lang.String).getMethod(charAt, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(c.d((CharSequence) Class.forName(java.lang.String).getConstructor(Class.forName("[B")).newInstance(Class.forName(android.util.Base64).getMethod(decode, Class.forName(java.lang.String), (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(null, Array.get(invoke, i), Class.forName(android.util.Base64).getDeclaredField(NO_WRAP).get(null))), i ^ 137), 0)).charValue();
                }
                b.c.a.a.class.getDeclaredFields()[0].set(null, bArr);
                return;
            }
            throw new NullPointerException();
        } catch (Throwable unused) {
            throw new RuntimeException();
        }
    }

이때, AES-CBC 암호화 알고리즘의 key와 초기 IV는 VaultService의 onCreate에서 생성된다.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="kind_of_magic">
        <item>UEBxWw==</item>
        <item>Sk5xVcOICw==</item>
        <item>bnRX</item>
        <item>S0BgWw==</item>
        <item>Nw==</item>
        <item>R0ZxRMOLElk=</item>
        <item>TkJhWw==</item>
        <item>dHZHdcOl</item>
        <item>eWRNYQ==</item>
        <item>bHRSeMOi</item>
        <item>R05tVw==</item>
        <item>d2hScA==</item>
        <item>T0xyVMOADQ==</item>
        <item>f2pQ</item>
        <item>Q0xsVw==</item>
        <item>Nw==</item>
    </string-array>
</resources>

R.array.kind_of_magic 배열을 사용하는데, 디코딩한 파일에서 res\values\arrays.xml 파일에서 확인할 수 있다.

kind_of_magic의 각 원소는 base64로 인코딩되어 있고, 반복문에서 각 원소를 디코딩하여 첫 번째 문자를 bArr에 저장한다.

모든 bArr 내 문자들을 조합한 스트링이 key와 IV에 해당한다.

 

3. b.c.a.a 클래스의 b 메소드

package b.c.a;

public class a {

    /* renamed from: a  reason: collision with root package name */
    public static byte[] f821a;

    public static byte[] a(byte[] bArr) {
        try {
            Object invoke = Class.forName(javax.crypto.Cipher).getMethod(getInstance, Class.forName(java.lang.String)).invoke(null, AES/CBC/PKCS5Padding);
            Object newInstance = Class.forName(javax.crypto.spec.SecretKeySpec).getConstructor(Class.forName(c.d("KË", -867135215)), Class.forName(java.lang.String)).newInstance(a.class.getDeclaredFields()[0].get(null), AES);
            Object newInstance2 = Class.forName(javax.crypto.spec.IvParameterSpec).getConstructor(Class.forName([B)).newInstance(a.class.getDeclaredFields()[0].get(null));
            Object obj = Class.forName(javax.crypto.Cipher).getDeclaredField(DECRYPT_MODE).get(null);
            Class.forName(javax.crypto.Cipher).getMethod(init, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null), Class.forName(java.security.Key), Class.forName(java.security.spec.AlgorithmParameterSpec)).invoke(invoke, obj, newInstance, newInstance2);
            return (byte[]) Class.forName(javax.crypto.Cipher).getMethod(doFinal, Class.forName(c.d("NI", -1487124972)).invoke(invoke, bArr);
        } catch (Throwable unused) {
            throw new RuntimeException();
        }
    }

   public static byte[] b(byte[] bArr) {
        try {
            Object invoke = Class.forName(javax.crypto.Cipher).getMethod(getInstance, Class.forName(java.lang.String)).invoke(null, AES/CBC/PKCS5Padding);
            Object newInstance = Class.forName(javax.crypto.spec.SecretKeySpec).getConstructor(Class.forName("[B"), Class.forName(java.lang.String)).newInstance(a.class.getDeclaredFields()[0].get(null), AES);
            Object newInstance2 = Class.forName(javax.crypto.spec.IvParameterSpec).getConstructor(Class.forName("[B")).newInstance(a.class.getDeclaredFields()[0].get(null));
            Object obj = Class.forName(javax.crypto.Cipher).getDeclaredField(ENCRYPT_MODE).get(null);
            Class.forName(javax.crypto.Cipher).getMethod(init, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null), Class.forName(java.security.Key), Class.forName(java.security.spec.AlgorithmParameterSpec)).invoke(invoke, obj, newInstance, newInstance2);
            return (byte[]) Class.forName(javax.crypto.Cipher).getMethod(doFinal, Class.forName("[B")).invoke(invoke, bArr);
        } catch (Throwable unused) {
            throw new RuntimeException();
        }
    }
}

VaultService의 onCreate()에서 구한 key와 IV인 PKnJ3BJqymGvKzG2로 암호화를 진행한다.

 

4. VaultService.class

Object invoke = Class.forName(android.util.Base64).getMethod(encode, Class.forName("[B"), (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(null, b2, Class.forName(android.util.Base64).getDeclaredField(NO_WRAP).get(null));
Object newInstance = Class.forName(java.lang.String).getConstructor(Class.forName("[B")).newInstance(invoke);

암호화 이후 해당 스트링을 Base64로 인코딩 후 스트링으로 변환한다.

Object invoke2 = Class.forName(android.content.Context).getMethod(getString, (Class) Class.forName(java.lang.Integer).getDeclaredField(TYPE).get(null)).invoke(VaultService.this, Integer.valueOf((int) R.string.magic));

이후 R.string.magic으로 스트링을 가져온다.

<string name="magic">7E3Q5fm4lBSKXaHTnlCO52VL/iY6f+hQQ35oeFphtZIu3pf0QuOEpFB5nTeg8GTx</string>

해당 magic이란 이름을 가지는 스트링은 res\values\strings.xml 파일에서 확인할 수 있다.

return ((Boolean) Class.forName(java.lang.String).getMethod(equals, Class.forName(java.lang.Object)).invoke(invoke2, newInstance)).booleanValue();

입력한 값을 AES로 암호화한 후 Base64로 인코딩한 스트링과, magic 스트링이 동일한지(equals 메소드 사용)에 대한 boolean 값을 반환한다.

 

 

 

Exploit


Flag는 다음의 과정으로 구할 수 있다.

1. magic을 Base64로 디코딩

2. b.c.a.a의 a 메소드를 이용하여 복호화

3. 2에서 얻은 스트링을 String 객체로 변환하여 출력하면 get flag!

 

flag_vault101.java

import java.lang.reflect.Array;
import java.util.Base64;

public class flag_vault101 {

	public static void main(String[] args) throws java.io.UnsupportedEncodingException, IllegalArgumentException, IllegalAccessException, SecurityException{
		// TODO Auto-generated method stub
		String kind_of_magic[] = {
				"UEBxWw==",
				"Sk5xVcOICw==",
				"bnRX",
				"S0BgWw==",
				"Nw==",
				"R0ZxRMOLElk=",
				"TkJhWw==",
				"dHZHdcOl",
				"eWRNYQ==",
				"bHRSeMOi",
				"R05tVw==",
				"d2hScA==",
				"T0xyVMOADQ==",
				"f2pQ",
				"Q0xsVw==",
				"Nw=="
		};
		String magic = "7E3Q5fm4lBSKXaHTnlCO52VL/iY6f+hQQ35oeFphtZIu3pf0QuOEpFB5nTeg8GTx";
		int length = kind_of_magic.length;
		byte[] bArr = new byte[length];
		Base64.Decoder decode = Base64.getDecoder();
		for(int i=0; i<length; i++) {
			Object obj = Array.get(kind_of_magic, i);
			byte[] invoke = decode.decode(obj.toString());
			String str = d(new String(invoke, "UTF-8"), i ^ 137);
			bArr[i] = ((byte) str.charAt(0));

		}
		a.class.getDeclaredFields()[0].set(null, bArr);
		Base64.Decoder decoder = Base64.getDecoder();
		byte[] magic_decode= decoder.decode(magic);
		byte[] result = a.a(magic_decode);
		System.out.println(new String(result));
	}
	public static int INT=1;

	public static char a(char c, int i) {
        return (char) (c & ((1 << i) ^ 65535));
    }

    public static char b(char c, int i) {
        return (char) (c | (1 << i));
    }

    public static char c(char c, int i) {
        return (char) ((c & (1 << i)) >> i);
    }

	public static String d(CharSequence charSequence, int i) {
        StringBuilder sb = new StringBuilder();
        if (i == 0) {
            return sb.toString();
        }
        for (int i2 = 0; i2 < charSequence.length(); i2++) {
            char charAt = charSequence.charAt(i2);
            char c = (char) (i >> (i2 % 4));
            int i3 = i2 % 3;
            if (i3 == 0) {
                for (int i4 = 0; i4 < 8; i4 += 2) {
                    int c2 = c(charAt, i4) ^ c(c, i4);
                    if (c2 == 0) {
                        charAt = a(charAt, i4);
                    } else if (c2 == 1) {
                        charAt = b(charAt, i4);
                    }
                }
            } else if (i3 == 1) {
                for (int i5 = 1; i5 < 8; i5 += 2) {
                    int c3 = c(charAt, i5) ^ c(c, i5);
                    if (c3 == 0) {
                        charAt = a(charAt, i5);
                    } else if (c3 == 1) {
                        charAt = b(charAt, i5);
                    }
                }
            } else if (i3 == 2) {
                for (int i6 = 0; i6 < 8; i6++) {
                    int c4 = c(charAt, i6) ^ c(c, i6);
                    if (c4 == 0) {
                        charAt = a(charAt, i6);
                    } else if (c4 == 1) {
                        charAt = b(charAt, i6);
                    }
                }
            }
            sb.append((char) (charAt ^ INT));
        }
        return sb.toString();
    }
}

 

a.java

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class a {
	public static byte[] f821a;
	public static byte[] a(byte[] bArr) {
		try {
			Cipher AES_CBC = Cipher.getInstance("AES/CBC/PKCS5Padding");
			SecretKeySpec key = new SecretKeySpec((byte[])a.class.getDeclaredFields()[0].get(null), "AES");
			IvParameterSpec IV = new IvParameterSpec((byte[])a.class.getDeclaredFields()[0].get(null));
			AES_CBC.init(Cipher.DECRYPT_MODE, key, IV);
			return (byte[]) AES_CBC.doFinal(bArr);
		}catch (Throwable unused) {
            throw new RuntimeException();
        }
	}
}

 

flag

SCTF{53CUr17Y_7Hr0U6H_085CUr17Y_15_N07_3N0U6H}

 

Verify flag

 

728x90
반응형

'REVERSING > Android' 카테고리의 다른 글

Android device connection setting in ubuntu  (0) 2021.09.09
Android Device Rooting (Magisk)  (0) 2021.09.09
[Android Malware] Anubis Analysis  (0) 2021.08.02
[Google CTF 2020] android  (0) 2021.07.21
[Android Malware] CovidLock analysis  (0) 2021.07.19

관련글 더보기