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.sodvmJNIUseBridge函数下断点的方法

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

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

然后使用StegSolve进行处理

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

Last updated