初见
附件为一个apk,在模拟器中运行一下,主界面:
随便输入一个字符串,点击GO:
弹框显示:Failed。
没有其它信息,反编译看看。
反编译
修改apk后缀名为zip,解压:
使用dex2jar对解压得到的“classes.dex”文件进行反编译:
使用jd-gui查看反编译的代码。
直接看MainActivity,代码量不大,一眼就可以看到弹框的字符串所在函数:
public void onGoClick(View paramView) {
String str = this.etFlag.getText().toString();
if (getSecret(getFlag()).equals(getSecret(encrypt(str)))) {
Toast.makeText((Context)this, "Success", 1).show();
return;
}
Toast.makeText((Context)this, "Failed", 1).show();
}
所以正确的输入应该满足:
getSecret(getFlag())返回的字符串的内容等于getSecret(encrypt(str))返回的字符串的内容。
这里面str是我们输入的字符串。
这里有三个关键函数:getSecret/getFlag/encrypt,我们一个一个分析。
getSecret
不管异常处理部分,核心代码为:
byte[] arrayOfByte = MessageDigest.getInstance(encrypt("KE3TLNE6M43EK4GM34LKMLETG").substring(5, 8)).digest(paramString.getBytes("UTF-8"));
if (arrayOfByte != null) {
StringBuilder stringBuilder = new StringBuilder(arrayOfByte.length * 2);
int j = arrayOfByte.length;
for (int i = 0; i < j; i++) {
byte b = arrayOfByte[i];
if ((b & 0xFF) < 16)
stringBuilder.append("0");
stringBuilder.append(Integer.toHexString(b & 0xFF));
}
return stringBuilder.toString();
}
分三步:
- 使用MessageDigest类的实例计算得到arrayOfByte
- 申请arrayOfByte两倍长度的string
- 将arrayOfByte转换为16进制字符串保存在string内
而这个MessageDigest是常用的包,能搜到很多资料,是用于计算消息摘要的包。但这里使用的是哪个消息摘要算法得看:
encrypt("KE3TLNE6M43EK4GM34LKMLETG").substring(5, 8)
这里取个巧,不去读encrypt的算法,直接修改smali代码得到encrypt函数的返回值。修改步骤为:
1、使用apktool对apk进行解包:
2、修改MainActivity.smali,在encrypt函数调用后打印返回值:
invoke-virtual {p0, v7}, Lcom/ph0en1x/android_crackme/MainActivity;->encrypt(Ljava/lang/String;)Ljava/lang/String;
move-result-object v8
invoke-static {v8, v8}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
3、重新打包apk
4、重新签名apk
在模拟器里安装新生成的apk,打开Android Studio,运行apk,随便输入字符串,点击GO。在Android Studio底部的logcat窗口中就可以看到输出结果了:
结果字符串的5-8字符为:"MD5",也就是getSecret就是就计算MD5。
而正确的输入是使得,getSecret(getFlag())返回的字符串的内容等于getSecret(encrypt(str))返回的字符串的内容。也就是两个字符串的MD5值相同,考虑到MD5碰撞概率极低,这里相当于要求getFlag()等于encrypt(str)。
接下来我们还是使用修改smali代码的方法,看看getFlag返回的字符串是什么,修改的流程与上面一直,修改的位置变为MainActivity.smali里getFlag函数调用后,通过插入Log函数打印返回值:
invoke-virtual {p0}, Lcom/ph0en1x/android_crackme/MainActivity;->getFlag()Ljava/lang/String;
move-result-object v1
invoke-static {v8, v8}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
logcat中看到的结果为:
到这里,总结一下,我们现在需要让输入的字符串经过encrypt处理后,变为 “ek`fz@q2^x/t^fn0mF^6/^rb`qanqntfg^E`hq|”
encrypt
从反编译结果可以看出,这是一个jni函数:
public native String encrypt(String paramString);
到apk解压后的lib子目录下,可以看到多个处理器架构的库:
找熟悉的x86看,使用IDA打开里面的libphcm.so。
找到encrypt函数,F5看反编译的结果:
int __cdecl Java_com_ph0en1x_android_1crackme_MainActivity_encrypt(int a1, int a2, int a3)
{
size_t v3; // esi
const char *s; // edi
v3 = 0;
for ( s = (const char *)(*(int (__cdecl **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a3, 0); v3 < strlen(s); --s[v3++] )
;
return (*(int (__cdecl **)(int, const char *))(*(_DWORD *)a1 + 668))(a1, s);
}
这里是面向对象程序,a1是this指针,这里面调用了a1的两个成员函数a1+676和a1+668这两个是啥函数,暂不知道。
但分析里面的for循环,确很简单,就是给字符串每个字符-1。
我们的输入内容经过encrypt函数处理后为“ek`fz@q2^x/t^fn0mF^6/^rb`qanqntfg^E`hq|”,如果只是被for循环每个字符-1,我们就只需要给每个字符+1。每个字符加一的结果为:
flag{Ar3_y0u_go1nG_70_scarborough_Fair}
这里就无需多言了~~~
———————————————————————————————————————————
欢迎关注我的微博:大雄_RE。专注软件逆向,分享最新的好文章、好工具,追踪行业大佬的研究成果。