2016 ZCTF Android1 200
Java层的代码比常规的CM多了不少
关键的就在点击事件里
public void attemptLogin() {
String username = this.mEmailView.getText().toString();
String password = this.mPasswordView.getText().toString();
View focusView;
if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) {
this.mPasswordView.setError("Password Too Short");
focusView = this.mPasswordView;
} else if (TextUtils.isEmpty(username)) {
this.mEmailView.setError("User name is NULL");
focusView = this.mEmailView;
} else if (!isEmailValid(username)) {
this.mEmailView.setError("Error");
focusView = this.mEmailView;
} else if (new Auth().auth(this, username + password, databaseopt()) == 1) {
Toast.makeText(getApplicationContext(), getString(R.string.Auth_Success), 0).show();
OpenNewActivity(password);
} else {
Toast.makeText(getApplicationContext(), getString(R.string.Auth_Fail), 0).show();
}
}用户名和密码长度合法性校验
校验函数auth()里移动有三个参数,第二个参数是用户名和密码的结合,第三个参数由下面这个函数计算而出
找到路径
前面先将key.db文件拷贝到目标路径,然后读取,读取的SQL语句如下
直接用工具读取出数据库的值

调用验证函数,先获取username + password字符串逆序的字节数组,调用了encrypt()函数进行加密,然后读取flag_bin文件和加密后的数据进行对比
这个加密函数使用了DES加密算法
那么我们可以直接读取flag_bin文件,使用秘钥zctf2016进行解密,从而查看密码是多少
由于密文长度的问题,一共只有16字节,但是分配了64字节的空间,解密会报错,所以我手动改成了16,或者定义数组的时候使用读取的长度作为参数传进去
输出

看来是要继续看代码
我们发现它在校验完密码后,会跳转到另一个Activity,在经过分析后,这个Activity里的东西貌似才是关键
横竖都退出
接下来是一个Native函数,目测是反调试
跟到so,符号都没混淆,程序猿编程习惯不错
大概是在进行TracerPid检测反调试

再将一个文件读到目标文件夹下
最后调用另一个Native函数进行处理,传入的是刚才计算出来的密码{Notthis}
该函数不复杂,简单看一下函数调用,目测是进行DES加解密的计算,入口处理了传入的密码,存储为字节数组
申请堆空间,将前面获取的密码字节数组存储到堆中
释放掉临时密码数组的内存,同时再次调用TracerPid检测反调试函数
这log输出的是啥玩意,后面申请了一个比较大的堆空间
申请堆空间成功后,进行堆的初始化,拷贝了一个256字节的Table过去,同时打开文件/data/data/com.zctf.app/files/bottom,这个文件在Java层做了拷贝操作
在做完准备工作后,进行DES解密,密文前256字节存储在so中,秘钥是传入的密码前8字节
这里提供两种方法查看解密后的数据
第一种是手动模拟计算
以为接下来我会撸一波代码秀一波加解密吗
呵呵,我是那种喜欢撸代码的人吗
其实我开始用C撸了一遍解密,然而写挫了,解密的数据有点小问题
直接进入第二种方法,我们可以发现解密完后的数据直接就释放掉了,也就是说,内存中有那么一瞬间存在着解密后的数据
所以,可以通过动态调试,把那片内存撸出来,同时为了可以动态调试,我们需要先过掉反调试
也就是要修改APK,改的方法有很多,组合也非常多种,比我晚上撸串的选择都多
这里提供一种我个人的方案
以修改最少为原则,删删删这种方法我不是很喜欢
退出函数把退出的那句代码删掉
下面TracerPid反调试的代码这里不修改,我们可以修改检测函数的返回值,而不是在调用的时候改
这样Java层的反调试就绕过了,如果跑起来,效果大概是这样的

原因我们在前面的代码中分析过,解密后的堆数据直接就释放了
侧面说明,解密后的数据是一张图片
接下来修改Native层,这里需要额外多注意一点,这个so有JNI_OnLoad函数
将BL j_j_ptracepatch掉,或者做全套,前面的参数赋值全都patch掉
IDA的Edit->Patch Program->Change byte可以实现直接修改so的功能
可以看到这一句是4字节,所以使用00 00 00 00来替换,效果如下

但是这时函数尾识别出错了,需要修复一下函数
使用右键Edit Function,修改函数尾部地址

修改完

另外一处校验是TracerPid检测反调试,我们使用一种优雅的方式去处理
修改这个函数的返回值就行
接下来记得apply change
替换源so文件,重打包签名,进行动态调试
如果碰到动态调试断不下来,可以使用在libdvm.so的dvmJNIUseBridge函数下断点的方法

释放前下断,找到R0指向的堆空间,可以看到解密出来的是一个PNG文件

知道起始地址,整个堆空间长度是0x1460,我们可以直接用脚本拷贝这片堆数据,走一个
然后使用StegSolve进行处理

最后,有个很玄学的问题,为了找到为什么一开始写代码解密会出错的原因,我特意看了一下秘钥

Last updated