Samsung "Hacker's Playground 2020" CTF Vault 101 分析
Challenge : Find the password. File given : Vault101–1.1-release.apk Obfuscation Level = High Number of Solves = 15
首先使用jadx打开apk. 找到入口。com.sctf2020.vault101.MainActivity. 快速定位到关键函数onClick:
public void onClick(View view) { try { boolean a = this.f3457s.mo3513a(this.f3454p.getText().toString()); Toast toast = new Toast(this); toast.setView(getLayoutInflater().inflate(a ? 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 (!a) { this.f3454p.getText().clear(); } } catch (RemoteException e) { e.printStackTrace(); } }
如果this.f3457s.mo3513a返回真的话,则成功。否则失败。
f3457s是在ServiceConnectionC0974a类中实例化的。
跟过来找到mo3513a方法,
注意 AbstractC0919b.AbstractBinderC0920a只是一个接口,我们应该找到此类的实现类。最后我们在VaultService找到了mo3513a的实现方法。这里Activity与Service使用了android里面跨进程通信binder机制。
所以,我们在输入的flag最终会调用service里面的mo3513a方法,跟进发现混淆相当严重。但还好的是调用的同一个函数C0922c.m2647d进行解密
C0922c 此类实现如下,发现比较复杂 。直接上idea,调用试试,
package p058b.p092c.p093a; /* renamed from: b.c.a.c */ public class C0922c { /* renamed from: a */ public static int f3120a; /* renamed from: a */ public static char m2644a(char c, int i) { return (char) (c & ((1 << i) ^ 65535)); } /* renamed from: b */ public static char m2645b(char c, int i) { return (char) (c | (1 << i)); } /* renamed from: c */ public static char m2646c(char c, int i) { return (char) ((c & (1 << i)) >> i); } /* renamed from: d */ public static String m2647d(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 = m2646c(charAt, i4) ^ m2646c(c, i4); if (c2 == 0) { charAt = m2644a(charAt, i4); } else if (c2 == 1) { charAt = m2645b(charAt, i4); } } } else if (i3 == 1) { for (int i5 = 1; i5 < 8; i5 += 2) { int c3 = m2646c(charAt, i5) ^ m2646c(c, i5); if (c3 == 0) { charAt = m2644a(charAt, i5); } else if (c3 == 1) { charAt = m2645b(charAt, i5); } } } else if (i3 == 2) { for (int i6 = 0; i6 < 8; i6++) { int c4 = m2646c(charAt, i6) ^ m2646c(c, i6); if (c4 == 0) { charAt = m2644a(charAt, i6); } else if (c4 == 1) { charAt = m2645b(charAt, i6); } } } sb.append((char) (charAt ^ f3120a)); } return sb.toString(); } }
String x = C0922c.m2647d(";È\u0003p¯ 4ŶorÂ\"Ý\u0010|", -500953648); System.out.println(x);
果然没有辣么简单。输出来的是乱码。
分析了下C0922c.m2647d方法,发现其中有个变量f3120a 没有初始化,搜一下看在哪被赋值了。
原来在Application中偷偷初始化了。搞安卓开发的同学应该比较熟悉这个,Application启动比其它组件更早。一般用来全局共享变量啥的,在app整个生命周期只有一份。
然后稍微改一下, 给f3120a初始化下
public static int f3120a=1;
嗯,可以解密成功了。
然后就应该写个程序去解密。
嗯。写了半天,放弃了,直接人肉 alt+f8大法。
去完混淆之后,VaultService类里面的两个方法大概如下,基本都是通过反射实例化类,然后调用相关的方法。。
public static boolean mo3513a(String str) {//str是我们输入的字符串 try { int i ; valut.f3462a = 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; //如果i>3 则system.exit(0); } else if (str == null) { return false; } else {// byte[] b = C0918a.m2640b((byte[]) Class.forName("java.lang.String").getMethod("getBytes", new Class[0]).invoke(str, new Object[0]));//把我们输入的字符串转为byte之后调用了 C0918a.m2640b方法 Object invoke = Class.forName("android.util.Base64").getMethod("encode", Class.forName("[B"), (Class) Class.forName("java.lang.Integer").getDeclaredField("TYPE").get(null)).invoke(null, b, Class.forName("android.util.Base64").getDeclaredField("NO_WRAP").get(null)); //对 C0918a.m2640b返回来的base64 encode Object newInstance = Class.forName("java.lang.String").getConstructor(Class.forName("[B")).newInstance(invoke);//返回再转为byte. Object invoke2 = Class.forName("android.content.Context").getMethod("getString", (Class) Class.forName("java.lang.Integer").getDeclaredField("TYPE").get(null)).invoke(valut.class, Integer.valueOf((int) R.string.magic)); //取出R.string.magic ; return ((Boolean) Class.forName("java.lang.String").getMethod("equals", Class.forName("java.lang.Object")).invoke(invoke2, newInstance)).booleanValue();//比较R.string.magic和我们输入的是否相等。 } } catch (Throwable unused) { throw new RuntimeException(); } } 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]), 1);//Integer.valueOf((int) R.array.kind_of_magic)); //getResources().getStringArray(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(C0922c.m2647d((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();//这一大坨的代码就是把kind_of_magic数组里面的东西取出来然后base64 decode再调用C0922c.m2647d。 } C0918a.class.getDeclaredFields()[0].set(null, bArr); //保存到类变量中。 return; } throw new NullPointerException(); } catch (Throwable unused) { throw new RuntimeException(); } }
到此时,我们大概能推出。程序把我们输入的flag调用C0918a.m2640b处理,然后与自身保存的值进行比较,如果一致,则说明我们输入正确,否则失败。我们先看下C0918a.m2640b做了啥。找到此类
如下
然后又是一阵苦力活,得到
/* renamed from: a */ public static byte[] m2639a(byte[] bArr) { //DECRYPT_MODE 解密 try { Object invoke = javax.crypto.Cipher.getInstance( "AES/CBC/PKCS5Padding"); Object newInstance = Class.forName("javax.crypto.spec.SecretKeySpec").getConstructor(Class.forName("[B"), Class.forName("java.lang.String")).newInstance(C0918a.class.getDeclaredFields()[0].get(null), "AES"); Object newInstance2 = Class.forName("javax.crypto.spec.IvParameterSpec").getConstructor(Class.forName("[B")).newInstance(C0918a.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("[B")).invoke(invoke, bArr); } catch (Throwable unused) { throw new RuntimeException(); } } /* renamed from: b */ public static byte[] m2640b(byte[] bArr) { //ENCRYPT_MODE 加密 try { Object invoke = Class.forName("javax.crypto.Cipher").getMethod("getInstance", Class.forName("getInstance")).invoke(null, "AES/CBC/PKCS5Padding"); Object newInstance = Class.forName("javax.crypto.spec.SecretKeySpec").getConstructor(Class.forName("[B"), Class.forName("java.lang.String")).newInstance(C0918a.class.getDeclaredFields()[0].get(null),"AES"); Object newInstance2 = Class.forName("javax.crypto.spec.IvParameterSpec").getConstructor(Class.forName("[B")).newInstance(C0918a.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(); } }
原来就是aes加解密。
所以程序把我们输入的字符串进行aes加密后与R.string.magic对比,如果一样,则成功。否则失败。所以我们只需要解密magic就可以了。在res\strings.xml文件中找到这个值
有了密文,我们还差密钥。我们看下上面的m2639a方法也就是DECRYPT_MODE 解密方法。注意在实例化javax.crypto.spec.SecretKeySpe时,传入的是C0918a.class.getDeclaredFields()[0].get(null), 而我们在上面的VaultService类中的onCreate方法中又看到给C0918a.class.getDeclaredFields()[0]进行了赋值,那么这个值就是密钥,同样在实例化iv时也传入的这个值。那么我们需要去找到这个值 。
在上面注释中我们写了onCreate就是把kind_of_magic数据处理了下(数组里面的东西取出来然后base64 decode再调用C0922c.m2647d)。
文件位置:
那我们写个函数处理下;
String kind_of_magic[] = {"UEBxWw==", "Sk5xVcOICw==", "bnRX", "S0BgWw==", "Nw==", "R0ZxRMOLElk=", "TkJhWw==", "dHZHdcOl", "eWRNYQ==", "bHRSeMOi", "R05tVw==", "d2hScA==", "T0xyVMOADQ==", "f2pQ", "Q0xsVw==", "Nw=="}; byte[] deByted = new byte[kind_of_magic.length]; for(int i=0;i<kind_of_magic.length; i++){ Object obj = Array.get(kind_of_magic, i); byte[] deByte =Base64.getDecoder().decode(obj.toString()); String deString = C0922c.m2647d(new String(deByte), i^137); System.out.println(deString); deByted[i] = (byte) deString.charAt(0); } // C0918a.class.getDeclaredFields()[0].set(null, deByted); System.out.println(new String(deByted)); }
运行输出,得到密钥。
得到flag