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
浙公网安备 33010602011771号